Project 02: The Effects of Initial Velocity Angle and Nucleic Charge on Saturnian Orbital Trajectories¶

Date: October 20, 2025

Name: Rhea Zhu

Table of Contents¶

1. Project Overview
  • 1. Project 02: Effects of Initial Velocity Angle and Nucleic Charge
    • 1.1 Overview
      • 1.1.1 Experimental Method & Analysis
        • 1.1.1.1 Simulation Introduction
        • 1.1.1.2 Defining a Stable Orbit
        • 1.1.1.3 Programming Analysis
          • 1.1.1.3.1 Programming with Solve_IVP
        • 1.1.1.4 Analysis Methods
        • 1.1.1.5 Physical Assumptions
2. Analysis of Combined Parameters
  • 2. Analysis: Combinations of Initial Velocity Angle and N₂ Charge
    • 2.1 Part I: Preliminary Orbital Trajectory Analysis
      • 2.1.1 Cycle 1: ($N_2 = N_1$), varying $\theta$
      • 2.1.2 Cycle 2: ($N_2 = 5N_1$), varying $\theta$
      • 2.1.3 Cycle 3: ($N_2 = 10N_1$), varying $\theta$
      • 2.1.4 Cycle 4: ($N_2 = 20N_1$), varying $\theta$
      • 2.1.5 General Analysis — Part I
    • 2.2 Part II: Discrete Phase-Space Analysis to Detect Stable Orbits
      • 2.2.1 Programming for stability detection
      • 2.2.2 Parameters
      • 2.2.3 Detecting Stable Orbits
      • 2.2.4 Examples of Periodicity Check
      • 2.2.5 Discrete Phase Space Simulation
    • 2.3 Part III: Continuous Phase-Space Analysis to Quantify Relative Standard Deviation
      • 2.3.1 Analysis for Continuous Quantitative Phase Space Scan
      • 2.3.2 Pattern 1: Periodic Collisions
      • 2.3.3 Pattern 2: Claw-Mark Collisions
      • 2.3.4 Pattern 3: Neon Band above Lower Thir Region
      • 2.3.5 Pattern 4: Curves of Periodicity
    • 2.4 Part IV: Numerical Artifacts and Quantification of stability
3. Limitations and Future Work
  • 3. Results & Discussion
    • 3.1 Part I General Analysis
    • 3.2 Part II General Analysis
    • 3.3. Limitations & Next Steps
      • 3.3.1 Limitations
      • 3.3.2 Next Steps
4. Acknowledgements, References, & Appendices
  • 4. Acknowledgements
  • 5. References
Appendix: Code Validation¶
  • 6. Appendix 1: Code Validation
Appendix: Reflection Questions¶
  • 7. Appendix 2: Reflection Questions

Overview¶

In this project, I use Solve_IVP to investigate the conditions needed to achieve stable Saturnian trajectories for an electron orbiting two nuclei of variable charge. Specifically, I vary the electron's initial velocity angle and distance and ask the following:

Research Questions

  1. What electron path trajectories are traced when varying (1) initial velocity angle, and (2) nucleic charge?
  2. What combinations of initial velocity angle and nucleic charge create stable orbital trajectories?

Experimental Method & Analysis¶

Simulation Introduction¶

210Diagram

Figure 1. Schematic of simulation setup.

  1. Setup: Two nuclei, call $N_1$ and $N_2$, are vertically placed $1.2 \times 10^{-10}m$ apart. An electron $e$ is placed $5.29 \times 10^{-11}m$ from the point bisecting $N_1$ and $N_2$. $N_1$ has a fixed nucleic charge of $-e^- = 1.6 \times 10^{-19} C$, while $N_2$ is variable
  2. Initial Parameters: In this simulation, two parameters are explored: initial electron velocity and $N_2$ nucleic charge:
    • Initial electron velocity angle: In this experiment, we restrict the domain of $\theta$ to $0 \leq \theta \leq 180^o$. We choose to restrict $\theta$ to two quadrants, since we expect the electron trajectories of domain $\theta$ to $0 \leq \theta \leq 180^o$ to be symmetrical to $0 \leq \theta \leq -180^o$.
    • $N_2$ nucleic charge: Since $N_2$ is a nucleus, its charge is restricted to positive values. So, the range of values for $N_2$ charge, call $c$, is $(c > 0) C$.
  3. Start Simulation: When the simulation begins, the electron orbits at a speed of $2.18 \times 10^6 m/s$ around the two nuclei; this speed was determined by an electron's speed around a hydrogen atom a Bohr radius away from the nucleus. The electron's trajectory follows the Coulomb force [1]: $$F_{orbit} = \frac{q_1 q_2}{4 \pi \epsilon_0 r^2} \vec{r}$$ However, since the electron is in the presence of two nuclei, its resultant force is the vector sum of both Coulomb forces. So, if we let $c$ be the charge of $N_2$. then $$F_{total} = \frac{e^- c}{4 \pi \epsilon_0 r_{N_2^2}} \vec{r_{N_2}} + \frac{-e^2}{4 \pi \epsilon_0 r_{N_1^2}} \vec{r_{N_1}}$$ The electron orbits for $5$ seconds before the simulation is stopped.
  4. Outcome: By varying the initial parameters, we analyze what parametric combinations create stable and unstable orbits. Note that stable orbits are periodic and predictable, whereas unstable orbits are random and chaotic.

Note that we define $\theta$ as the angle the velocity vector makes with the horizontal x-axis; that is, $90^o$ corresponds to the vector parallel to the line defined by $N_1$ and $N_2$

Defining a stable orbit¶

We quantitatively define a stable orbit in two ways:

  1. Periodicity: The relative standard deviation between all periods is within 10%. For each time step during a simulation run, the distance between the electron’s current position and its original position is calculated, and the local minima of these distances are found; the time intervals between the local minima represent approximate “periods” of the electron’s trajectory. Using this, the relative standard deviation between the time intervals is calculated; if it is less than 10%, the orbit is deemed periodic.

  2. Electron-proton collision: At any time step, if he electron is within 8.5e-16m of either proton, the orbit is unstable. Note that 8.5e-16 m was chosen, since it is the radius of the proton. Therefore, if the electron’s position is within its radius, it is assumed to have collided with the proton.

If the electron trajectory is both periodic and does not collide with the proton, we say that the orbit is stable.

Programming Analysis¶

Programming with Solve_IVP [2]¶

In this project, we will use the function Solve_IVP to simulate the electron's path trajectories. Solve_IVP is a function in Python's SciPy library, designed to solve ordinary differential equations (ODEs) in initial value problems (IVPs). To do this, Solve_IVP takes initial variables (such as position and velocity) and calls a function specified by the user to calculate the derivatives of those initial states; then, Solve_IVP takes these equations to calculate position and velocity at each specified time step, using a technique similar to Euler's method.

This is useful for the simulation, since we are modelling the components for position $x, y$, velocity $v_x, v_y$, acceleration $a_x, a_y$, and force $F_x, F_y$, where $$F_x = ma_x, a_x = \frac{dv_x}{dt}, v_x = \frac{dx}{dt}$$ and $$F_y = ma_y, a_y = \frac{dv_y}{dt}, v_y = \frac{dy}{dt}$$

To use Solve_IVP, we first define the initial state of our electron, with the tuple $(x_0, y_0, v_x0, v_y0)$. We then create a function specifying the differential relationships between these four values. Using the equation for Coulomb force: $$F = \frac{q_1q_2}{4 \pi r^2}$$ We can calculate the component forces on the electron at each time step, then use that to calculate acceleration. The derivative for $x_0$ and $y_0$ woudl then be $v_{x0}$ and $v_{y0}$ respectively, and the derivatives for $v_{x0}$ and $v_{y0}$ are $v_{a0}$ and $a_{y0}$. We return these four values from our function, which Solve_IVP then uses to calculate the trajectory of the electron.

We execute each simulation over a time span of $1e-14$s. This value was determined via several initial simulation tests over a large range of nucleic charge and initial velocity angle combinations; it was found that 1e-14s was the most appropriate period that both (1) accurately represents the full trajectory of the electrons, and (2) is not too long as to overly prolong the code runtime.

Analysis Methods¶

This project's analysis will be broken into four portions.

  1. Preliminary Trial Analysis: In this section, we manually alter the $N_2$ and $\theta$ parameters and qualitatively observe the trajectories of the electrons as they shift. By conducting these preliminary trials, we gain general knowledge of how nucleic charge and initial velocity angle affect electron orbital trajectories. Note that stability is not tested here, since we want to first get a preliminary understanding of the effects of nucleic charge and angles on electron orbital paths.
  2. Discrete Longitudinal Phase Space Analysis: In this section, we quantitatively determine if orbits are stable over a wide range in our phase space by running our simulation with $\theta$ and $N_2$ combinations throughout our domains of $0 \leq \theta \leq 90^o$ and $\frac{1}{100}N_1\leq N_2 \leq 100N_1$. We choose the $N_2$ range based on the maximum value of $N_2$ tested in the preliminary trials, which is 100. We take the inverse of 100 for the lower bound of our $N_2$ domain. We also choose our $\theta$ to be restricted to our first quadrant to limit the program's run time; from preliminary trial analysis, the behavior of obtuse angles is very similar to the behavior of acute angles, so we do not expect this restriction to compromise our project's validity.The discrete phase space analysis outputs which combinations of initial velocity angel and $\frac{N_2}{N_1}$ parameters leads to (1) stable, (2) unstable/quasi-periodic, and (3) collision orbits
  3. Continuous Longitudinal Phase Space Analysis: In Part III, we extend about our discrete phase space by running a continuous phase space analysis on the relative standard deviation of each combination of our phase space parameters; we also increase the resolution of our phase space to more deeply understand the patterns seen in our simulations. This portion of our analysis goes deeper into the justification of stable, unstable, and collision orbits, and alo checks the validity of our quantification of stability.
  4. Numerical Artifacts and Quantification of stability: In this final section, we look at the numerical artifacts present in our continuous longitudinal phase space graph and try to understand why such artifacts are present. We reflect on these results and connect them to possible limitations to our quantification of stability.

Physical Assumptions¶

The following assumptions are made in our simulation:

  1. Two Exclusive Forces: The only two forces acting on the electron are those of $N_1$ and $N_2$. We neglect the gravitational forces the electron experiences from $N_1$ and $N_2$, since they are magnitudes weaker than electrostatic force.
  2. Classical Interpretation: The simulation assumes an entirely classical interpretation of the setup, using Coulomb force as the main driver of this project.
  3. Neglect Relativistic Effects: Since the electron's velocity is magnitudes smaller than the speed of light, we can also reasonably neglect relativistic effects.
  4. Point-Like: We treat the three particles as point-like figures.

Analysis: Investigating Combinations of Initial Velocity Angle and N2 Charge¶

Part I: Preliminary Trial Analysis To Understand Orbital Trajectories¶

In Part I, we manually choose various combinations of $N_2$ and $\theta$ to run the simulation, observing the qualitative characteristics of the electron trajectory.

For this portion of analysis, we separate it into four cycles. We firstly set $N_2 = N_1$ and alter $\theta$. We then simulate electron trajectories when $N_2$ is larger, that is, when $N_2 = 5N_1$, and also alter $\theta$. For cycle 3, we do the opposite and set $N_2 = \frac{1}{5}N_1$, and for cycle 4, we let $N_2$ be much larger than $N_1$ where $N_2 = 100N_1$. By picking various ratios of $N_2$ and $N_1$, we take a very broad range of data to capture most of the possible electron path trajectories in the simulation.

Note that the $\theta$ we pick to alter for each cylce was determined after initial testing, where each $\theta$ analyzed below shows a unique pattern of characteristics.

Cycle 1: $N_2 = N_1$, Altering $\theta$¶

For our first cycle, we let $N_2 = N_1$, and set $\theta = 5^o, 20^o, 90^o, 120^o$.

Trial 1: $\theta = 5^o$¶

In [2]:
# SIMULATION CODE

# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
import math 

# Constants 

k = 8.99e9            # Coulomb constant, N·m²/C²
e = -1.6e-19          # Electron charge magnitude, C
n1 = 1.6e-19          # Nucleus 1 charge 
me = 9.109e-31        # Electron mass, kg
r0 = 5.29e-11         # Bohr radius, m 

distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
y1 = distance / 2  # N1 y coordinate, m 
x1 = 0             # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0             # N2 y coordinate, m 


def get_trajectory(n2, angle, T):
    # Initial Velocity
    v0 = 2.18e6  # initial velocity (m/s)  
    theta = math.radians(angle) #convert angle to radians, rad
    vx0 = math.cos(theta) * v0  # initial x velocity (m/s)
    vy0 = math.sin(theta) * v0  # initial y velocity (m/s)
    state0 = (r0, 0, vx0, vy0)  # (x0, y0, vx0, vy0) (m,m,m/s,m/s)

    t_span = (0, T) # span for simulation to run 
    

    # Nuclei coordinates
    # We define the coordinate (0,0) to be the halfway point between N1 and N2
    distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
    y1 = distance / 2  # N1 y coordinate, m 
    x1 = 0             # N1 x coordinate, m
    y2 = -distance / 2 # N2 x coordinate, m
    x2 = 0             # N2 y coordinate, m

    # Get your solution from solve_ivp    
    sol1 = solve_ivp(diff_eqns, t_span, state0, rtol=1e-9, atol=1e-9) 
    print(sol1)
    return sol1

# Define your diff_eqns function
def diff_eqns(t, state):
    x, y, vx, vy = state #state variables in timestep, m, m, m/s, m/s

    
    r1x = x - x1 #x distance between electron & N1, m
    r1y = y - y1 #y distance between electron & N1, m
    r1 = np.sqrt(r1x**2+r1y**2) #distance between N1 and electron

    r2x = x - x2 #x distance between electron & N2, m
    r2y = y - y2 #y distance between electron & N2, m 
    r2 = np.sqrt(r2x**2+r2y**2) #distance between N2 and electron
    

    # Calculate the magnitude of the force 

    # Calculate force & acceleration components for N1
    fx1 = k * e * n1 * r1x / r1**3
    fy1 = k * e * n1 * r1y / r1**3

    # Calculate force & acceleration components for N1
    fx2= k * e * n2 * r2x / r2**3
    fy2 = k * e * n2 * r2y / r2**3 

    fx = fx1 + fx2 
    fy = fy1 + fy2

    # Calculate acceleration
    accx = fx/me 
    accy = fy/me  

    # Return differentials
    return vx, vy, accx, accy 

def plot_trajectory(sol1, top, T, c):
    # Plot y vs x for theta = 5
    fig, ax = plt.subplots(1, 3)
    fig.set_size_inches(14, 9)
    fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
    plt.subplots_adjust(top=top) 

    ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
    ax[2].set_aspect("equal") 
    ax[2].set_title(f"Time Span = {T} s") 
    ax[2].set_xlabel("x (m)")
    ax[2].set_ylabel(" y (m) ")
    ax[2].grid(True) 
    ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[2].legend(loc='upper left', fontsize=9) 

    # Get the one-third index
    idx1 = len(sol1.t) // 3

    # Plot only the first one-third of the data
    ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
    ax[1].set_aspect("equal") 
    ax[1].set_title(f"Time Span = (1/3) * {T} s") 
    ax[1].set_xlabel("x (m)")
    ax[1].set_ylabel(" y (m) ")
    ax[1].grid(True) 
    ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[1].legend(loc='upper left', fontsize=9) 

    # Get the one-tenth index
    idx2 = len(sol1.t) // 8

    # Plot only the first one-third of the data
    ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
    ax[0].set_aspect("equal") 
    ax[0].set_title(f"Time Span = (1/8) * {T}s") 
    ax[0].set_xlabel("x (m)")
    ax[0].set_ylabel(" y (m) ")
    ax[0].grid(True) 
    ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[0].legend(loc='upper left', fontsize=9) 

    # Find global min and max for x and y
    x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
    y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

    # Apply limits to all subplots
    for a in ax:
        a.set_xlim(x_min, x_max)
        a.set_ylim(y_min, c * y_max)

    plt.show()

Code Block Summary: This block creates the functiosn to calculate the trajectory of the electron given n2 and angle, using Solve_IVP and the differential equations function, which models Coulomb force. A function for plotting trajectory is also created.

In [2]:
# Parameters to vary
n2 = 1.6e-19  # Nucleus 2 charge
angle = 5   # Launch angle in degrees
T = 5e-15 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.643e-18 ...  5.000e-15  5.000e-15]
        y: [[ 5.290e-11  5.640e-11 ... -7.929e-11 -7.959e-11]
            [ 0.000e+00  3.123e-13 ...  2.018e-11  2.015e-11]
            [ 2.172e+06  2.087e+06 ... -1.509e+06 -1.501e+06]
            [ 1.900e+05  1.901e+05 ... -1.270e+05 -1.270e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 29240
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 2. The electron follows a trajectory first to $N_1$, then loops around to $N_2$. The density of the electron's trajectory is significantly higher towards the $y=0$ axis, and overall traces an ellipsoid-like shape.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 5$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 2: $\theta = 20^o$¶

Next, we investigate electron trajectories when $N_1$ and $N_2$ have equal charges while varying $\theta$.

In [3]:
# Parameters to vary
n2 = 1.6e-19  # Nucleus 2 charge
angle = 20   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.673e-18 ...  4.999e-15  5.000e-15]
        y: [[ 5.290e-11  5.625e-11 ...  1.017e-10  1.021e-10]
            [ 0.000e+00  1.247e-12 ... -4.251e-11 -4.194e-11]
            [ 2.049e+06  1.962e+06 ...  4.277e+05  3.965e+05]
            [ 7.456e+05  7.462e+05 ...  5.781e+05  5.826e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 38888
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 3. Similar to $\theta = 20$, electron follows a trajectory first to $N_1$, then loops around to $N_2$. The cycles in the electron's path are much closer together, indicating that the oribit is more periodic.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 20$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 3: $\theta = 90^o$¶

In [4]:
# Parameters to vary
n2 = 1.6e-19  # Nucleus 2 charge
angle = 90   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.1, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.915e-23 ...  5.000e-15  5.000e-15]
        y: [[ 5.290e-11  5.290e-11 ...  8.553e-12  8.175e-12]
            [ 0.000e+00  4.174e-17 ...  2.065e-11  2.194e-11]
            [ 1.335e-10 -1.000e+00 ... -9.347e+05 -9.499e+05]
            [ 2.180e+06  2.180e+06 ...  3.166e+06  3.214e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 119396
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 4. Again, the electron follows a trajectory first to $N_1$, then loops around to $N_2$. The overall density of the electron's path is larger and the semi-major axis is much higher.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 4: $\theta = 120^o$¶

Finally, we investigate electron trajectories when $N_1$ and $N_2$ have equal charges while varying $\theta$.

In [5]:
# Parameters to vary
n2 = 1.6e-19  # Nucleus 2 charge
angle = 120   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1,1.2, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.461e-18 ...  5.000e-15  5.000e-15]
        y: [[ 5.290e-11  5.125e-11 ... -2.008e-13 -1.409e-13]
            [ 0.000e+00  2.759e-12 ...  4.462e-11  4.450e-11]
            [-1.090e+06 -1.167e+06 ...  2.397e+06  2.397e+06]
            [ 1.888e+06  1.889e+06 ... -4.905e+06 -4.879e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 68720
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 5. The electron follows a trajectory first to $N_1$, then loops around to $N_2$. Whiel it traces on overall ellptical shape like the others, compared to the other angles, this trajectory's ellipsoid is more spherical.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 120$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Cycle 1 Analysis¶

From cycle 1, we notice the following:

  • Ellipsoid: Across all four trials, the electron trajectories trace an ellipsoid pattern, where the electron first loops up towards $N_1$, down towards $N_2$, and back up to $N_1$, and repeats.
  • Semi-Major Axis: As the angle nears 90, the semimajor axis of an ellipsoid increases. This is due to the increase in the electron's vertical; since the y-component of electron velocity is highest at 90 degrees, so is the y-distance the electron travels.
  • Angle: While the angle does not change the overall shape the electron traces, we see that the "density" of the shape changes. For instance, when $\theta = 20$, the electron paths are much denser and periodic. Furthermore, when $\theta = 5$, we see a density increase along the $y=0$ axis, since the initial horizontal velocity component is large.

Cycle 2: $N_2 = 5N_1$, Altering $\theta$¶

For our second cycle, we let $N_2 = 5N_1$, and we set $\theta = 5^o, 90^o, 135^o$.

Trial 1: $\theta = 5^o$¶

We investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 5^o$.

In [6]:
# Parameters to vary
n2 = 5 * 1.6e-19  # Nucleus 2 charge
angle = 5   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.43, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  9.683e-19 ...  5.000e-15  5.000e-15]
        y: [[ 5.290e-11  5.493e-11 ...  5.267e-11  5.269e-11]
            [ 0.000e+00  1.294e-13 ... -4.487e-11 -4.387e-11]
            [ 2.172e+06  2.021e+06 ...  1.447e+05  4.602e+04]
            [ 1.900e+05  7.829e+04 ...  4.152e+06  4.127e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 107762
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 6. The electron follows a trajectory that solely orbits $N_2$. Due to this, the distance the electron travels each period is much shorter, indicating a smaller period. As a result, the density of the electron's trajectory is high, and overall traces a bowl-like shape.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 5$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 2: $\theta = 90^o$¶

We investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 90^0$.

In [7]:
# Parameters to vary
n2 = 5 * 1.6e-19  # Nucleus 2 charge
angle = 90   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  6.382e-24 ...  4.999e-15  5.000e-15]
        y: [[ 5.290e-11  5.290e-11 ... -4.648e-11 -4.702e-11]
            [ 0.000e+00  1.391e-17 ...  2.934e-11  2.995e-11]
            [ 1.335e-10 -1.000e+00 ... -6.755e+05 -5.679e+05]
            [ 2.180e+06  2.180e+06 ...  7.454e+05  6.893e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 77594
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 7. The electron follows a trajectory solely orbiting $N_2$. However, due to its initial y-velocity component resulting from $\theta = 90$, the electron reaches closer to $N_1$ before reversing direction towards $N_2$. This results in a partial-ellipsoid like shape.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 3: $\theta = 135^o$¶

Finally, we investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 135^0$.

In [8]:
# Parameters to vary
n2 = 5 * 1.6e-19  # Nucleus 2 charge
angle = 135   # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.4, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  9.637e-19 ...  4.999e-15  5.000e-15]
        y: [[ 5.290e-11  5.134e-11 ... -6.092e-11 -6.115e-11]
            [ 0.000e+00  1.430e-12 ...  9.130e-12  9.742e-12]
            [-1.541e+06 -1.691e+06 ... -3.150e+05 -2.016e+05]
            [ 1.541e+06  1.427e+06 ...  7.322e+05  6.569e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 102638
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 8. Again, the electron follows a trajectory solely orbiting $N_2$. We see that the bowl-like curve is roughly halfway between when $\theta = 0$ and $\theta = 90$. This is expected, since the initial vertical velocity component is in between those of the two angles.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 135$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Cycle 2 Analysis¶

From cycle 2, we notice the following:

  • Partial Ellipsoid: Across all three trials, we see that a partial ellipsoid is traced by the electron trajectories. This is due to the strong Coluomb force of $N_2$; since $N_2 = 5N_1$, the force of $N_2$ on the electron is significantly more than $N_1$, leading the electron to only orbit $N_2$. However, this coefficient of $5$ is not large enough such that the electron's orbit creates a whole ellipsoid around $N_2$, indicating to effect of $N_1$ on the electron; instead, we see that the electron traces bowl-like shapes.
  • Curve of Partial Ellipsoid: We see that as $\theta$ nears 90 degrees, the curve of the partial ellipsoid increases, such that the electron is approaches $N_1$ more closely. This can be explained due to the initial velocity components of teh electron. At $\theta = 90$, the electron's velocity is purely in the y-direction. As a result, it takes a longer time for the $N_2$ Coulomb force to decelerate the electron and reverse its direction back towards $N_2$, allowing the electron to travel closer to $N_1$ each cycle.
  • Electron Path Density: We see that the curves traced by the electron trajectory are much denser than in cycle 1. This is due to its orbital path; due to the large $N_2$, the electron is confinedd to only orbit $N_2$, meaning that the distance travelled per period is less. So, for the same time span, the electron travels more periods, increasing trajectory density.

Cycle 3: $N_2 = \frac{1}{5}N_1$, Altering $\theta$¶

For our third cycle, we let $N_2 = \frac{1}{5}N_1$, and we set $\theta = 70, 90, 135$.

Trial 1: $\theta = 70^o$¶

We investigate electron trajectories when $N_2 = \frac{1}{5}N_1$ have equal charges and $\theta = 70^o$.

In [9]:
# Parameters to vary
n2 = 0.2 * 1.6e-19  # Nucleus 2 charge
angle = 70   # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.906e-18 ...  1.500e-14  1.500e-14]
        y: [[ 5.290e-11  5.426e-11 ...  7.681e-11  7.772e-11]
            [ 0.000e+00  3.949e-12 ... -8.773e-11 -8.262e-11]
            [ 7.456e+05  6.838e+05 ...  2.377e+05  1.881e+05]
            [ 2.049e+06  2.094e+06 ...  1.178e+06  1.223e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 90656
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 9. Again, the electron orbits both $N_1$ and $N_2$ in a ring-like shape. We see that the ring is more spherical due to the horizontal velocity component; since there is less y component in the electron's speed, it does not travel past $N_1$ and $N_2$ as much as instead immediately reverses direction.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 70$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 2: $\theta = 90^o$¶

We investigate electron trajectories when $N_2 = \frac{1}{5}N_1$ have equal charges and $\theta = 90^o$.

In [10]:
# Parameters to vary
n2 = 0.2 * 1.6e-19  # Nucleus 2 charge
angle = 90   # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  3.191e-23 ...  1.500e-14  1.500e-14]
        y: [[ 5.290e-11  5.290e-11 ...  1.133e-10  1.136e-10]
            [ 0.000e+00  6.957e-17 ... -4.142e-11 -4.011e-11]
            [ 1.335e-10 -1.000e+00 ...  2.407e+05  2.271e+05]
            [ 2.180e+06  2.180e+06 ...  1.147e+06  1.155e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 49634
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 10. The electron follows a trajectory that solely orbits both $N_1$ and $N_2$. Unlike Cycles 1 and 2, the path that the electron traces is ring-like, where it only travels outside a 2-dimensional ellipse.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectory is made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.

Trial 3: $\theta = 135^o$¶

Finally, we investigate electron trajectories when $N_2 = \frac{1}{2}N_1$ have equal charges and $\theta = 135^0$.

Figure 10. Contrasting trials 1 and 2, we notice that the electron does not trace a ring-like pattern anymore. This, again, is due to the lower vertical velocity; since there is less of a y component in the electron's velocity, the electron does not have enough speed to pass $N_1$ and $N_2$ before being "pulled into" the nuclei.

In [11]:
# Parameters to vary
n2 = 5*0.2 * 1.6e-19  # Nucleus 2 charge
angle = 135  # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.2, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.670e-18 ...  1.500e-14  1.500e-14]
        y: [[ 5.290e-11  5.025e-11 ... -2.319e-11 -2.318e-11]
            [ 0.000e+00  2.575e-12 ... -5.716e-11 -5.748e-11]
            [-1.541e+06 -1.629e+06 ...  4.340e+04  7.763e+04]
            [ 1.541e+06  1.543e+06 ... -4.240e+06 -4.243e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 154856
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 11. The electron traces an elliptical-like journey, similar to Cycle 1. The lack of enough vertical velocity deters the electron to move past each proton before reversing direction in a ring shape.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 135$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.

Code Block Summary: This block creates three subplots for the electron's trajectory. The three time plots represent different tiem spans to highlight both the path the electron takes and the overall shape it traces.

Cycle 3 Analysis¶

From cycle 3, we notice the following:

  • Ring Shape: When $\theta$ is close to 90, we see that the electron traces a ring-like shape. This is due to the large y-velocity component; there is enough vertical speed for the electron to travel past the protons before reversing direction. This behavior is only seen when $N_2$ has a small nucleic charge, reducing its Coulomb force on the electron. As $\theta$ moves away from 90, the ring thickens due to the decrease in y-velocity.
  • Diverging from a Ring: Eventually, when $\theta$ is no longer dear 90 degrees, we see that the electron no longer traces a ring-like shape, and instead establishes a pattern similar to what is seen in Cycle 2.

Cycle 4: $N_2 = 100N_1$, Altering $\theta$¶

For our final cycle, we let $N_2 = 100N_1$, and we set $\theta = 90, 70, 45$.

Trial 1: $\theta = 90^o$¶

We investigate electron trajectories when $N_2 = 500N_1$ have equal charges and $\theta = 90^o$.

In [3]:
def get_trajectory(n2, angle, T):
    # Initial Velocity
    v0 = 2.18e6  # initial velocity (m/s)  
    theta = math.radians(angle) #convert angle to radians, rad
    vx0 = math.cos(theta) * v0  # initial x velocity (m/s)
    vy0 = math.sin(theta) * v0  # initial y velocity (m/s)
    state0 = (r0, 0, vx0, vy0)  # (x0, y0, vx0, vy0) (m,m,m/s,m/s)

    t_span = (0, T) # span for simulation to run 
    

    # Nuclei coordinates
    # We define the coordinate (0,0) to be the halfway point between N1 and N2
    distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
    y1 = distance / 2  # N1 y coordinate, m 
    x1 = 0             # N1 x coordinate, m
    y2 = -distance / 2 # N2 x coordinate, m
    x2 = 0             # N2 y coordinate, m

    # Get your solution from solve_ivp    
    sol1 = solve_ivp(diff_eqns, t_span, state0, rtol=1e-9, atol=1e-9) 
    print(sol1)
    return sol1

# Define your diff_eqns function
def diff_eqns(t, state):
    x, y, vx, vy = state #state variables in timestep, m, m, m/s, m/s

    
    r1x = x - x1 #x distance between electron & N1, m
    r1y = y - y1 #y distance between electron & N1, m
    r1 = np.sqrt(r1x**2+r1y**2) #distance between N1 and electron

    r2x = x - x2 #x distance between electron & N2, m
    r2y = y - y2 #y distance between electron & N2, m 
    r2 = np.sqrt(r2x**2+r2y**2) #distance between N2 and electron
    

    # Calculate the magnitude of the force 

    # Calculate force & acceleration components for N1
    fx1 = k * e * n1 * r1x / r1**3
    fy1 = k * e * n1 * r1y / r1**3

    # Calculate force & acceleration components for N1
    fx2= k * e * n2 * r2x / r2**3
    fy2 = k * e * n2 * r2y / r2**3 

    fx = fx1 + fx2 
    fy = fy1 + fy2

    # Calculate acceleration
    accx = fx/me 
    accy = fy/me  

    # Return differentials
    return vx, vy, accx, accy 

def plot_trajectory1(sol1, top, T, c):
    # Plot y vs x for theta = 5
    fig, ax = plt.subplots(1, 3)
    fig.set_size_inches(14, 9)
    fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
    plt.subplots_adjust(top=top) 

    ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
    ax[2].set_aspect("equal") 
    ax[2].set_title(f"Time Span = {T} s") 
    ax[2].set_xlabel("x (m)")
    ax[2].set_ylabel(" y (m) ")
    ax[2].grid(True) 
    ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[2].legend(loc='upper left', fontsize=9) 

    # Get the one-third index
    idx1 = len(sol1.t) // 3

    # Plot only the first one-third of the data
    ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
    ax[1].set_aspect("equal") 
    ax[1].set_title(f"Time Span = (1/3) * {T} s") 
    ax[1].set_xlabel("x (m)")
    ax[1].set_ylabel(" y (m) ")
    ax[1].grid(True) 
    ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[1].legend(loc='upper left', fontsize=9) 

    # Get the one-tenth index
    idx2 = len(sol1.t) // 8

    # Plot only the first one-third of the data
    ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
    ax[0].set_aspect("equal") 
    ax[0].set_title(f"Time Span = (1/8) * {T}s") 
    ax[0].set_xlabel("x (m)")
    ax[0].set_ylabel(" y (m) ")
    ax[0].grid(True) 
    ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[0].legend(loc='upper left', fontsize=9) 

    # Find global min and max for x and y
    x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
    y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

    # Apply limits to all subplots
    for a in ax:
        a.set_xlim(x_min, x_max)
        a.set_ylim(y_min, c * y_max)

    plt.show()
In [13]:
# Parameters to vary
n2 = 100 * 1.6e-19  # Nucleus 2 charge
angle = 90   # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory1(sol1, 1.2, T, 10) 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  3.791e-25 ...  2.500e-16  2.500e-16]
        y: [[ 5.290e-11  5.290e-11 ...  4.150e-11  4.172e-11]
            [ 0.000e+00  8.265e-19 ... -4.514e-11 -4.481e-11]
            [ 1.335e-10 -1.000e+00 ...  1.269e+07  1.248e+07]
            [ 2.180e+06  2.180e+06 ...  1.888e+07  1.880e+07]]
      sol: None
 t_events: None
 y_events: None
     nfev: 36386
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 12. The electron follows a trajectory that solely orbits $N_2$. Unlike Cycle 2, since the force on $N_2$ is magnitudes larger tha $N_1$, the effect on $N_1$ is negligible, making teh electron follow an elliptical path around the electron. Note that due to the initial conditions of the electron, (such as velocity), the electron's paths still drift, forming a shape resemblimg a thick rubber band.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.

Trial 2: $\theta = 45^o$¶

We investigate electron trajectories when $N_2 = 100N_1$ have equal charges and $\theta = 45^o$.

In [14]:
# Parameters to vary
n2 = 100 * 1.6e-19  # Nucleus 2 charge
angle = 45  # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.1, T, 80)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.448e-19 ...  2.500e-16  2.500e-16]
        y: [[ 5.290e-11  5.310e-11 ...  4.492e-11  4.512e-11]
            [ 0.000e+00  1.925e-13 ... -3.531e-11 -3.499e-11]
            [ 1.541e+06  1.161e+06 ...  9.704e+06  9.536e+06]
            [ 1.541e+06  1.119e+06 ...  1.621e+07  1.611e+07]]
      sol: None
 t_events: None
 y_events: None
     nfev: 44780
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 13. Again, the electron orbits both $N_2$ in an ellipse. However, we notice that the elliptical "hole" in the previous trial is not present anymore. This indicates that the drift velocity of every period is lower.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 45$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.

Figure 13. Again, the electron orbits both $N_2$ in an ellipse. Drift is further reduced.

Trial 3: $\theta = 20$¶

We investigate electron trajectories when $N_2 = 100N_1$ have equal charges and $\theta = 20^o$.

In [15]:
# Parameters to vary
n2 = 100 * 1.6e-19  # Nucleus 2 charge
angle = 20  # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.2, T, 80)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.321e-19 ...  2.500e-16  2.500e-16]
        y: [[ 5.290e-11  5.315e-11 ...  4.294e-11  4.341e-11]
            [ 0.000e+00  7.298e-14 ... -3.545e-11 -3.477e-11]
            [ 2.049e+06  1.701e+06 ...  1.130e+07  1.093e+07]
            [ 7.456e+05  3.596e+05 ...  1.630e+07  1.608e+07]]
      sol: None
 t_events: None
 y_events: None
     nfev: 48146
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 14. Again, the electron orbits both $N_2$ in an ellipse. However, the electron experiences less drift to the the the angle.

Code Block Summary: This block simulates the electron's trajectory when $\theta = 20$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.

Cycle 4 Analysis¶

From cycle 4, we notice the following:

  • Flat Ellipsoid: The electron only orbits $N_2$ in an ellptic shape due to its large force compared to $N_1$. Crucially, teh electron's trajectory is different from that of cycle 1; while electrons form a full, 3-dimensional ellipsoid in cycle 1, the electron creates a flat, rubber-band-like shape in cycle 4. This is due to the drift the electron experiences across time
  • Symmetric electron path: Unlike in the other cycles, cycle 4 results show how the electron's trajectory every period is unchaotic; its 3-dimensional shape is due not to the variations within one period, but due to the electron's drift.
  • Drift: As we alter the angle, the electron's trajectory shift changes, due to the respective y and x components of initial velocity. Specifically, as $\theta$ moves away from 90 degrees, the drift decreases.

Part II: Discrete Phase-Space Analysis to Detect Stable Orbits¶

In Part II, we add additional event detection to quantitatively detect stable orbits across a phase space of $N_2$ and $\theta$. In this phase space, we define three categories of orbits:

  1. Stable Periodic: These orbits have relative standard deviations of under 10%, and do not collide with either nucleus.
  2. Unstable/Quasi-Periodic: These orbits have relative deviations of over 10%, but also do not collide with either nucleus.
  3. Collision: These orbits collides with at least 1 of the 2 nuclei. As a result, periodicity is not checked.

Programming for stability detection¶

To determine stability, we must check for (1) periodicity and (2) electron-proton collision. To check for periodicity, we create a function that calculates the negative distance between the electron and its original position for each time step. We then use SciPy’s “find_peaks” function to determine the local minima of each of the distances; since the function finds the local maxima, we insert the negative of each distance value into an array into "find_peaks" to determine the local minima. The times of each of these distances is then indexed out of the solution array outputted by Solve_IVP, and the periods are calculated by subtracting the times from each other. The relative standard deviation is then calculated and compared to the 5% threshold.

For electron-proton collision, we create functions to calculate the distance between the electron and both protons at all time-steps. Each distance is then compared with the distance threshold of 8.5e-16m to determine if an electron-proton collision occurs.

Parameters¶

We set each simulation to run for $2.5 e -15$ s; this was the longest time the code could run for the phase space to output results in less than 1.5 hours. We run 9000 simulations in our phase space domains of $0 \leq \theta \leq 90^o$ and $\frac{1}{100}N_1 \leq N_2 \leq 100N_1$ by creating a 90-element linear space for $\theta$ and 100-element linear space for $N_2$. As a reminder, $\theta = 0$ refers to the initial velocity vector parallel with the positive x axis, and $\theta = 90$ refers to the initial velocity vector parallel with the line defined by $N_1$ and $N_2$.

Detecting Stable Orbits¶

The code below checks for stable orbits across the chosen phase space.

In [3]:
# SIMULATION CODE
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math 
import time

# Fixed Constants
k = 8.99e9            # Coulomb constant, N·m²/C²
e = -1.6e-19          # Electron charge magnitude, C
n1 = 1.6e-19          # Nucleus 1 charge 
me = 9.109e-31        # Electron mass, kg
r0 = 5.29e-11         # Bohr radius, m 

# Orbital time
T = 2.5e-15  # time for simulation to run, s

# Nuclei coordinates
# We define the coordinate (0,0) to be the halfway point between N1 and N2
distance = 1.2e-10  # vertical distance between two nuclei, m
y1 = distance / 2   # N1 y coordinate, m 
x1 = 0              # N1 x coordinate, m
y2 = -distance / 2  # N2 x coordinate, m
x2 = 0              # N2 y coordinate, m

# Initial Velocity
v0 = 2.18e6  # initial velocity (m/s)

# Define your diff_eqns function
def diff_eqns(t, state):
    x, y, vx, vy = state  # state variables in timestep, m, m, m/s, m/s
    
    r1x = x - x1  # x distance between electron & N1, m
    r1y = y - y1  # y distance between electron & N1, m
    r1 = np.sqrt(r1x**2 + r1y**2)  # distance between N1 and electron
    r2x = x - x2  # x distance between electron & N2, m
    r2y = y - y2  # y distance between electron & N2, m 
    r2 = np.sqrt(r2x**2 + r2y**2)  # distance between N2 and electron
    
    # Avoid singularities
    r1 = max(r1, 1e-20)
    r2 = max(r2, 1e-20)
    
    # Calculate force & acceleration components for N1
    fx1 = k * e * n1 * r1x / r1**3
    fy1 = k * e * n1 * r1y / r1**3
    # Calculate force & acceleration components for N2
    fx2 = k * e * n2 * r2x / r2**3
    fy2 = k * e * n2 * r2y / r2**3 
    fx = fx1 + fx2 
    fy = fy1 + fy2
    # Calculate acceleration
    accx = fx / me 
    accy = fy / me  
    # Return differentials
    return vx, vy, accx, accy

Code Block Summary: This above code sets the initial constants used in the rest of the simulation. It also defines the differential equations needed to be inputted into Solve_IVP, including vertical velocity, horizontal velocity, vertical acceleration, and horizontal acceleration. These were calculated using the equation for Coulomb's force.

In [4]:
# NEW DETECTION FUNCTIONS

def check_collision(sol, collision_threshold=8.5e-16):
    # Calculate distances to each nucleus at all time steps
    r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
    r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
    
    # Check collision with nucleus 1
    collision_n1_indices = np.where(r1 < collision_threshold)[0]
    # Check collision with nucleus 2
    collision_n2_indices = np.where(r2 < collision_threshold)[0]
    collided = False
    
    if len(collision_n1_indices) > 0:
        collided = True
    elif len(collision_n2_indices) > 0:
        collided = True
    return collided

Code Block Summary: The above code defines a function that checks for electron-proton collisions. For each time step, the function calculates the distance between the electron and its original position. Using this, it compares each difference to the collision threshold to output a boolean of whether the electron experiences a collision.

In [5]:
def check_periodicity(sol, rel_std_threshold=0.10):
    if len(sol.t) < 10:
        return False, None
    
    # Calculate distance from starting position
    x0, y0 = sol.y[0][0], sol.y[1][0]
    distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
    
    # Find local minima (returns to starting region)
    try:
        peaks, _ = find_peaks(-distances, distance=len(distances)//20)
    except:
        return False, None
    
    if len(peaks) < 2:
        return False, None
    
    # Calculate periods
    periods = np.diff(sol.t[peaks])
    
    if len(periods) < 2:
        return False, None
    
    # Calculate relative standard deviation
    period_mean = np.mean(periods)
    period_std = np.std(periods)
    period_std_rel = period_std / period_mean
    
    # Check if variation is within threshold
    is_periodic = period_std_rel < rel_std_threshold
    
    return is_periodic

Code Block Summary: The above code defines a function that checks for periodicity. For each time step, the function calculates the distacne between the electron and its original position. Using this, the local minima of the distances are calculated, and the times for each of these "peaks" are found. The difference between each time is then calculated to be the "period" of each cycle, and the relative standard deviation is calculated and compared to the threshold relative standard deviation.

In [6]:
def analyze_single_orbit(angle, n2_value):
    try:
        # Set up initial conditions
        theta = math.radians(angle)
        vx0 = math.cos(theta) * v0
        vy0 = math.sin(theta) * v0
        state0 = (r0, 0, vx0, vy0)
        
        # Set n2 as global for diff_eqns to use
        global n2
        n2 = n2_value
        
        # Run simulation
        t_span = (0, T)
        sol = solve_ivp(diff_eqns, t_span, state0, 
                       rtol=1e-9, atol=1e-9, 
                       max_step=1e-17)
        
        # Check for collision first
        collided = check_collision(sol)
        if collided:
            return 2  # Collision
        
        # Check periodicity
        is_periodic = check_periodicity(sol)
        if is_periodic:
            return 0  # Stable periodic
        else:
            return 1  # Unstable
            
    except Exception as e:
        return 3  # Error

Code Block Summary: The above code integrates the previous two functions to analyze a single orbit. The code is the same as the code used in Part I, except it incorporates the collision and periodicity checks insto a single function.

In [7]:
# PHASE SPACE SCAN 

def scan_phase_space_2d(n_theta=30, n_n2=40):
    print("="*70)
    print("2D PHASE SPACE SCAN: THETA vs N2")
    print("="*70)
    
    # Phase space ranges
    theta_range = np.linspace(0, 90, n_theta)  # 0 to 90 degrees
    n2_n1_ratios = np.logspace(-2, 2, n_n2)    # 0.01 to 100 (N1/100 to 100*N1)
    n2_range = n1 * n2_n1_ratios
    
    print(f"Theta axis: {n_theta} points from 0° to 90°")
    print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
    print(f"  N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
    print(f"  N2 range: {n2_range[0]:.3e} to {n2_range[-1]:.3e} C")
    print(f"Total simulations: {n_theta * n_n2}")
    print(f"Simulation time per orbit: {T:.2e} s")
    print("="*70 + "\n")
    
    # Initialize results grid
    results_grid = np.zeros((n_n2, n_theta))
    
    total_sims = n_theta * n_n2
    sim_count = 0
    start_time = time.time()
    
    # Run simulations
    for i, n2_val in enumerate(n2_range):
        print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
        for j, angle_val in enumerate(theta_range):
            sim_count += 1
            
            # Progress update every 5 simulations
            if sim_count % 5 == 0 or sim_count == total_sims:
                elapsed = time.time() - start_time
                rate = sim_count / elapsed if elapsed > 0 else 0
                eta = (total_sims - sim_count) / rate if rate > 0 else 0
                print(f"  Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
                      f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
            
            # Analyze this configuration
            status = analyze_single_orbit(angle_val, n2_val)
            results_grid[i, j] = status
    
    elapsed = time.time() - start_time
    print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
    print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
    
    # Print statistics
    n_stable = np.sum(results_grid == 0)
    n_unstable = np.sum(results_grid == 1)
    n_collision = np.sum(results_grid == 2)
    n_error = np.sum(results_grid == 3)
    
    print("Results Summary:")
    print(f"  Stable periodic:      {n_stable:4d} ({100*n_stable/total_sims:.1f}%)")
    print(f"  Unstable/quasi:       {n_unstable:4d} ({100*n_unstable/total_sims:.1f}%)")
    print(f"  Collision:            {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
    print(f"  Error:                {n_error:4d} ({100*n_error/total_sims:.1f}%)")
    print("="*70 + "\n")
    
    return results_grid, theta_range, n2_range

Code Block Summary: The above code incoroporates the previous code block to scan the entire phase space. It includes a progress check, where the progress rate is updated every 5 simulations. At the end, the function returns an array of all the results for each of the 9000 simulations.

In [8]:
# Plotting results

def plot_phase_space_2d(results_grid, theta_range, n2_range):
    from matplotlib.colors import ListedColormap
    
    fig = plt.figure(figsize=(14, 10))
    
    # Create meshgrid
    theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
    n2_n1_grid = n2_grid / n1
    
    # Define colormap
    colors = ["#6420a6", "#b5318c", "#f48d47", "#f6e724"]
    cmap = ListedColormap(colors)
    labels = ['Stable Periodic', 'Unstable/Quasi-periodic', 'Collision', 'Error']
    
    # Main 2D phase space plot
    ax = fig.add_subplot(111)
    
    im = ax.pcolormesh(theta_grid, n2_n1_grid, results_grid, 
                       cmap=cmap, vmin=0, vmax=3, shading='auto',
                       edgecolors='face', linewidth=0)
    
    ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
    ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
    ax.set_title('Phase Space: Orbital Stability Map\nθ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1] \n V0 = 2.18e6 m/s (Fixed) ', 
                 fontsize=16, fontweight='bold', pad=20)
    
    # Use log scale for N2/N1
    ax.set_yscale('log')
    
    # Set y-axis limits and ticks
    ax.set_ylim([0.01, 100])
    ax.set_yticks([0.01, 0.1, 1, 10, 100])
    ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
    
    # Set x-axis limits
    ax.set_xlim([0, 90])
    
    # Add grid
    ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
    
    # Add reference line at N2 = N1
    ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5, 
               alpha=0.8, label='N2 = N1 (equal charges)')
    
    # Add reference lines for key angles
    ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    
    # Create legend for stability regions
    from matplotlib.patches import Patch
    
    # Add colorbar
    cbar = plt.colorbar(im, ax=ax, ticks=[0.375, 1.125, 1.875, 2.625], 
                       pad=0.02, aspect=30)
    cbar.ax.set_yticklabels(labels, fontsize=10)
    cbar.ax.tick_params(size=0)
    
    # Add text annotations for interesting regions
    ax.text(5, 0.015, 'Low angle\nLow charge', fontsize=9, 
           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
    ax.text(85, 80, 'High angle\nHigh charge', fontsize=9, ha='right',
           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
    
    plt.tight_layout()
    plt.savefig('phase_space_2d_theta_vs_n2.png', dpi=300, bbox_inches='tight')
    print("Plot saved as 'phase_space_2d_theta_vs_n2.png'")
    plt.show()
    
    return fig

Code Block Summary: Given the grid of results calculated from the previous block, this function plots all the data into one graph and returns the figure.

Examples of Periodicity Check¶

As an example, below is a histogram of periods generated by the periodicity check function, when $\theta = 35$ and $N_2 = 0.8N_1$. This is a stable orbit.

In [ ]:
# Set up initial conditions


angle = 35
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 5e-15  # time for simulation to run, s

# Set n2 as global for diff_eqns to use
n2 = 0.8 * 1.6e-19

# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 15
Mean period: 3.08e-16 s
Standard deviation: 3.61e-19 s
Relative std dev: 0.12%
Minimum period: 3.07e-16 s
Maximum period: 3.08e-16 s
Range: 1.42e-18 s 

Is the orbit periodic? Yes
==================================================
No description has been provided for this image

Figure 15. A roughly-Gaussian distribution is seen for the orbital periods of a stable orbit. standard deviation is 0.12%.

On the contrary, below is a histogram of periods generated by the periodicity check function, when $\theta = 40$ and $N_2 = 50N_1$. This is a quasi-periodic, unstable orbit.

In [23]:
# Set up initial conditions


angle = 40
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 5e-15  # time for simulation to run, s

# Set n2 as global for diff_eqns to use
n2 = 60 * 1.6e-19

# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 18
Mean period: 2.76e-16 s
Standard deviation: 6.13e-17 s
Relative std dev: 22.21%
Minimum period: 1.72e-16 s
Maximum period: 3.55e-16 s
Range: 1.83e-16 s 

Is the orbit periodic? No
==================================================
No description has been provided for this image

Figure 16. A random distribution of orbital periods can be seen for an unstable orbit. Relative standard deviation is 22.21%.

We see that the periods for the latter is much more randomnly distributed, indicating a higher relative standard uncertainty and overall unstable orbit.

Part I Phase Space Simulation: Discrete Categories¶

In [24]:
# MAIN CODE

# Run phase space scan
print("\n" + "="*70)
input("Press Enter to start 2D phase space scan...")

results_grid, theta_range, n2_range = scan_phase_space_2d(n_theta=10, n_n2=10)

# Plot results
plot_phase_space_2d(results_grid, theta_range, n2_range)

# Save results
np.savez('phase_space_2d_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'phase_space_2d_results.npz'")
======================================================================
======================================================================
2D PHASE SPACE SCAN: THETA vs N2
======================================================================
Theta axis: 10 points from 0° to 90°
N2 axis: 10 points from N1/100 to 100*N1
  N2/N1 range: 0.010 to 100.0
  N2 range: 1.600e-21 to 1.600e-17 C
Total simulations: 100
Simulation time per orbit: 5.00e-15 s
======================================================================


N2/N1 = 0.010 (1/10)
  Progress: 10/100 (10.0%) | Rate: 13.2 sim/s | ETA: 7s
N2/N1 = 0.028 (2/10)
  Progress: 20/100 (20.0%) | Rate: 11.5 sim/s | ETA: 7s
N2/N1 = 0.077 (3/10)
  Progress: 30/100 (30.0%) | Rate: 9.7 sim/s | ETA: 7ss
N2/N1 = 0.215 (4/10)
  Progress: 40/100 (40.0%) | Rate: 7.8 sim/s | ETA: 8s
N2/N1 = 0.599 (5/10)
  Progress: 50/100 (50.0%) | Rate: 5.3 sim/s | ETA: 10s
N2/N1 = 1.668 (6/10)
  Progress: 60/100 (60.0%) | Rate: 3.6 sim/s | ETA: 11s
N2/N1 = 4.642 (7/10)
  Progress: 70/100 (70.0%) | Rate: 2.6 sim/s | ETA: 12s
N2/N1 = 12.915 (8/10)
  Progress: 80/100 (80.0%) | Rate: 1.6 sim/s | ETA: 12s
N2/N1 = 35.938 (9/10)
  Progress: 90/100 (90.0%) | Rate: 1.0 sim/s | ETA: 10s
N2/N1 = 100.000 (10/10)
  Progress: 100/100 (100.0%) | Rate: 0.7 sim/s | ETA: 0s

Scan completed in 143.4s (2.4 min)
Average: 1.43s per simulation

Results Summary:
  Stable periodic:        14 (14.0%)
  Unstable/quasi:         55 (55.0%)
  Collision:              31 (31.0%)
  Error:                   0 (0.0%)
======================================================================

Plot saved as 'phase_space_2d_theta_vs_n2.png'
No description has been provided for this image
Results saved to 'phase_space_2d_results.npz'

Code Block Summary. The above code is the main code function that executes the phase space scan, incorporating all previous functions. The graph si saved as an npz file after compiling.

Image of Phase-Space Simulation¶

FinalProduct

Figure 17. Image of the phase-space simulation. As the $N_2/N_1$ ratio increases, the number of periodic orbits decreases, and is replaced by unstable orbits and collisions.

From the phase space simulation, we see that as $\frac{N_2}{N_1}$ increases, the number of periodic orbits decreases and is replaced by unstable orbits, and as it further increases, those unstable orbits transition into collisions. However, we also see the effect of initial velocity angle on stabilities: specifically, there seems to be a curving band of stable orbits above the typical region, as well as an area on the right of the phase space of stable, unstable, and collision orbits. We pick 1 $N_2, \theta$ combination for each of these regions and observe their electron trajectories.

We first redefine analyze_single_orbit() to take period as a parameter. Note that the time spans we use to plot each region will be different; this is because we want to show a representative amount of the electron's trajectory for each plot, which we cannot do at a constant time span. However, the time span used to determine collisions and periodicity is constant.

In [ ]:
# Redefine analyze orbit with period as a parameter

def analyze_single_orbit(angle, n2_value,T):
    try:
        # Set up initial conditions
        theta = math.radians(angle)
        vx0 = math.cos(theta) * v0
        vy0 = math.sin(theta) * v0
        state0 = (r0, 0, vx0, vy0)
        
        # Set n2 as global for diff_eqns to use
        global n2
        n2 = n2_value
        # Run simulation
        t_span = (0, T)
        sol = solve_ivp(diff_eqns, t_span, state0, 
                       rtol=1e-9, atol=1e-9, 
                       max_step=1e-17)
        
        # Check for collision first
        collided = check_collision(sol)
        if collided:
            return 2, sol  # Collision
        
        # Check periodicity
        is_periodic = check_periodicity(sol)
        if is_periodic:
            return 0, sol  # Stable periodic
        else:
            return 1, sol  # Unstable
            
    except Exception as e:
        return 3, sol  # Error 

Region 1: Lower Third¶

Typical

We first analyze a trajectory in the lower third region of the phase space graph. We set $N_2 = 0.05N_1$ and $\theta = 20$.

In [ ]:
# Region 1: Typical Region
T = 2.5e-15 # time for simulation to run, s 
T1 = 2.5e-14
n2 = 0.05 * 1.6e-19 
angle = 20 
result, sol = analyze_single_orbit(angle, n2,T) 
if result == 0:
    print("Status: Stable periodic orbit")
elif result == 1:
    print("Status: Unstable orbit")
elif result == 2:
    print("Status: Collision with nucleus")

sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.4, T1, 1)
Status: Stable periodic orbit
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  2.416e-18 ...  2.500e-14  2.500e-14]
        y: [[ 5.290e-11  5.777e-11 ... -4.156e-11 -4.499e-11]
            [ 0.000e+00  1.881e-12 ...  2.474e-10  2.445e-10]
            [ 2.049e+06  1.982e+06 ... -7.161e+05 -7.084e+05]
            [ 7.456e+05  8.102e+05 ... -5.667e+05 -5.998e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 51710
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 18. Electron orbital trajectory when $N_2 = 0.05N_1$. Electron solely orbits $N_1$. and does so along elliptical paths that create a partial-ellipsoid like shape.

Region 2: Curving Band¶

Curve

Next, we analyze the region with curving band. We set $N_2 = 0.8N_1$ and $\theta = 35$.

In [ ]:
# Region 2: Curving Band
T = 2e-15  # time for simulation to run, s
n2 = 0.8 * 1.6e-19 
angle = 35
result, sol = analyze_single_orbit(angle, n2,T) 
if result == 0:
    print("Stable periodic orbit")
elif result == 1:
    print("Unstable orbit")
elif result == 2:
    print("Collision with nucleus")

T1 = 1e-14
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.25, T1, 1)
Stable periodic orbit
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.896e-18 ...  1.000e-14  1.000e-14]
        y: [[ 5.290e-11  5.620e-11 ... -1.965e-11 -2.033e-11]
            [ 0.000e+00  2.382e-12 ... -1.990e-11 -1.952e-11]
            [ 1.786e+06  1.697e+06 ... -2.561e+06 -2.547e+06]
            [ 1.250e+06  1.262e+06 ...  1.420e+06  1.406e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 77798
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 19. Electron orbital trajectory when $N_2 = 0.8N_1$ and $\theta = 35$. Electron orbits both $N_1$ and $N_2$ in a figure-eight pattern, tracing our an ellipsoid.

Region 3: Chaotic Region¶

Chaotic

Finally, we analyze the region of the phase space diagram with stable, unstable, and collision orbits.

In [ ]:
# Region 3: Chaotic Region
T = 2.5e-15  # time for simulation to run, s

n2 = 1.54e-19 #0.9625 * 1.6
angle = 86
result, sol = analyze_single_orbit(angle, n2,T) 

if result == 0:
    print("Stable periodic orbit")  

elif result == 1:
    print("Unstable orbit")
elif result == 2:
    print("Collision with nucleus")

T1 = 2.5e-15
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.1, T1, 1)
Stable periodic orbit
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.085e-18 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.304e-11 ... -4.548e-11 -4.525e-11]
            [ 0.000e+00  2.362e-12 ...  3.847e-11  3.897e-11]
            [ 1.521e+05  9.673e+04 ...  1.105e+06  1.126e+06]
            [ 2.175e+06  2.177e+06 ...  2.383e+06  2.388e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 57044
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 20. Electron trajectory when $N_2 = 0.9625 N_1$ and $\theta = 86$. Electron orbits both $N_1$ and $N_2$ in a loop, tracing out a whole three-dimensional ellipsoid shape.

While this orbit in the chaotic region seems very stable, we observe what occurs after alterative $N_2$ by $0.005e-19$ C.

In [ ]:
# Region 3: Chaotic Region
T = 2.5e-15  # time for simulation to run, s

n2 = 1.545e-19 #0.9625 * 1.6
angle = 86
result, sol = analyze_single_orbit(angle, n2,T) 

if result == 0:
    print("Stable periodic orbit")
elif result == 1:
    print("Unstable orbit")
elif result == 2:
    print("Collision with nucleus")

T1 = 2.5e-15
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1, T1, 1)
Collision with nucleus
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.083e-18 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.303e-11 ... -2.977e-12 -2.721e-12]
            [ 0.000e+00  2.356e-12 ...  3.404e-11  3.505e-11]
            [ 1.521e+05  9.643e+04 ...  1.009e+06  1.020e+06]
            [ 2.175e+06  2.177e+06 ...  3.980e+06  4.070e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 66332
     njev: 0
      nlu: 0
No description has been provided for this image

Figure 21. At $N_2 = 0.9656N_1$, the electron exhibits a collision with a nucleus.

Part III: Continuous Phase-Space Analysis to Quantify Relative Standard Deviation¶

In the final part of our analysis, we extend our discrete phase-space simulation into a quantitative heat map. Instead of showing the the stepwise categories of orbit, we display a continuous heat map of the relative standard deviations of each orbit in our phase space. However, if the orbit is a collision orbit, we override its value on the heat map with a solid red colour to indicate its collision. We first slightly alter our previous code to output a continuous heat map, the execute the main function.

In [2]:
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math 
import time

# Fixed Constants
k = 8.99e9            # Coulomb constant, N·m²/C²
e = -1.6e-19          # Electron charge magnitude, C
n1 = 1.6e-19          # Nucleus 1 charge 
me = 9.109e-31        # Electron mass, kg
r0 = 5.29e-11         # Bohr radius, m 

# Orbital time
T = 2.5e-15  # time for simulation to run, s

# Nuclei coordinates
distance = 1.2e-10  # vertical distance between two nuclei, m
y1 = distance / 2   # N1 y coordinate, m 
x1 = 0              # N1 x coordinate, m
y2 = -distance / 2  # N2 x coordinate, m
x2 = 0              # N2 y coordinate, m

# Initial Velocity
v0 = 2.18e6  # initial velocity (m/s)

# Define your diff_eqns function
def diff_eqns(t, state):
    x, y, vx, vy = state
    
    r1x = x - x1
    r1y = y - y1
    r1 = np.sqrt(r1x**2 + r1y**2)
    r2x = x - x2
    r2y = y - y2
    r2 = np.sqrt(r2x**2 + r2y**2)
    
    # Avoid singularities
    r1 = max(r1, 1e-20)
    r2 = max(r2, 1e-20)
    
    # Calculate force & acceleration components for N1
    fx1 = k * e * n1 * r1x / r1**3
    fy1 = k * e * n1 * r1y / r1**3
    # Calculate force & acceleration components for N2
    fx2 = k * e * n2 * r2x / r2**3
    fy2 = k * e * n2 * r2y / r2**3 
    fx = fx1 + fx2 
    fy = fy1 + fy2
    # Calculate acceleration
    accx = fx / me 
    accy = fy / me  
    # Return differentials
    return vx, vy, accx, accy

# REVISED DETECTION FUNCTIONS

def check_collision(sol, collision_threshold=8.5e-16):
    r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
    r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
    
    collision_n1_indices = np.where(r1 < collision_threshold)[0]
    collision_n2_indices = np.where(r2 < collision_threshold)[0]
    
    if len(collision_n1_indices) > 0 or len(collision_n2_indices) > 0:
        return True
    return False

def calculate_period_std(sol):
    # Calculate distance from starting position
    x0, y0 = sol.y[0][0], sol.y[1][0]
    distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
    
    # Find local minima (returns to starting region)
    try:
        peaks, _ = find_peaks(-distances, distance=len(distances)//20)
    except:
        return None
    
    if len(peaks) < 2:
        return None
    
    # Calculate periods
    periods = np.diff(sol.t[peaks])
    
    if len(periods) < 2:
        return None
    
    # Calculate relative standard deviation
    period_mean = np.mean(periods)
    period_std = np.std(periods)
    
    if period_mean == 0:
        return None
    
    period_std_rel = period_std / period_mean
    
    return period_std_rel

def analyze_single_orbit_quantitative(angle, n2_value):
    try:
        # Set up initial conditions
        theta = math.radians(angle)
        vx0 = math.cos(theta) * v0
        vy0 = math.sin(theta) * v0
        state0 = (r0, 0, vx0, vy0)
        
        # Set n2 as global for diff_eqns to use
        global n2
        n2 = n2_value
        
        # Run simulation
        t_span = (0, T)
        sol = solve_ivp(diff_eqns, t_span, state0, 
                       rtol=1e-9, atol=1e-9, 
                       max_step=1e-17)
        
        # Check for collision first
        collided = check_collision(sol)
        if collided:
            return -1  # Collision marker
        
        # Calculate period variability
        rel_std = calculate_period_std(sol)
        return rel_std
            
    except Exception as e:
        return None  # Error

# PHASE SPACE SCAN 

def scan_phase_space_2d_quantitative(n_theta=30, n_n2=40):
    print("="*70)
    print("2D QUANTITATIVE PHASE SPACE SCAN: Period Variability")
    print("="*70)
    
    # Phase space ranges
    theta_range = np.linspace(0, 90, n_theta)
    n2_n1_ratios = np.logspace(-2, 2, n_n2)
    n2_range = n1 * n2_n1_ratios
    
    print(f"Theta axis: {n_theta} points from 0° to 90°")
    print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
    print(f"  N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
    print(f"Total simulations: {n_theta * n_n2}")
    print("="*70 + "\n")
    
    # Initialize results grid (use NaN for missing data)
    results_grid = np.full((n_n2, n_theta), np.nan)
    
    total_sims = n_theta * n_n2
    sim_count = 0
    start_time = time.time()
    
    # Run simulations
    for i, n2_val in enumerate(n2_range):
        print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
        for j, angle_val in enumerate(theta_range):
            sim_count += 1
            
            if sim_count % 5 == 0 or sim_count == total_sims:
                elapsed = time.time() - start_time
                rate = sim_count / elapsed if elapsed > 0 else 0
                eta = (total_sims - sim_count) / rate if rate > 0 else 0
                print(f"  Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
                      f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
            
            # Analyze this configuration
            rel_std = analyze_single_orbit_quantitative(angle_val, n2_val)
            results_grid[i, j] = rel_std if rel_std is not None else np.nan
    
    elapsed = time.time() - start_time
    print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
    print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
    
    # Print statistics
    n_collision = np.sum(results_grid == -1)
    n_valid = np.sum(~np.isnan(results_grid) & (results_grid != -1))
    n_error = np.sum(np.isnan(results_grid))
    
    valid_data = results_grid[(~np.isnan(results_grid)) & (results_grid != -1)]
    
    print("Results Summary:")
    print(f"  Valid orbits:         {n_valid:4d} ({100*n_valid/total_sims:.1f}%)")
    print(f"  Collisions:           {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
    print(f"  Errors:               {n_error:4d} ({100*n_error/total_sims:.1f}%)")
    
    if len(valid_data) > 0:
        print(f"\nPeriod Rel. Std. Dev. Statistics (valid orbits):")
        print(f"  Min:     {np.min(valid_data):.6f}")
        print(f"  Max:     {np.max(valid_data):.6f}")
        print(f"  Mean:    {np.mean(valid_data):.6f}")
        print(f"  Median:  {np.median(valid_data):.6f}")
    
    print("="*70 + "\n")
    
    return results_grid, theta_range, n2_range

# PLOTTING RESULTS

def plot_phase_space_heatmap(results_grid, theta_range, n2_range):
    import matplotlib.colors as mcolors
    
    fig = plt.figure(figsize=(14, 10))
    
    # Create meshgrid
    theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
    n2_n1_grid = n2_grid / n1
    
    # Separate collision and non-collision data
    collision_mask = (results_grid == -1)
    data_for_heatmap = results_grid.copy()
    data_for_heatmap[collision_mask] = np.nan  # Hide collisions from main heatmap
    
    ax = fig.add_subplot(111)
    
    # Plot main heat map (period variability)
    im = ax.pcolormesh(theta_grid, n2_n1_grid, data_for_heatmap,
                       cmap='viridis', shading='auto',
                       vmin=0, vmax=np.nanpercentile(data_for_heatmap, 95))
    
    # Overlay collision regions in red
    collision_data = np.where(collision_mask, 1, np.nan)
    ax.pcolormesh(theta_grid, n2_n1_grid, collision_data,
                  cmap=mcolors.ListedColormap(['#ff0000']), 
                  shading='auto', alpha=0.9)
    
    ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
    ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
    ax.set_title('Phase Space: Orbital Period Variability (Rel. Std. Dev.)\n' +
                 'θ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1], V0 = 2.18e6 m/s', 
                 fontsize=16, fontweight='bold', pad=20)
    
    # Use log scale for N2/N1
    ax.set_yscale('log')
    ax.set_ylim([0.01, 100])
    ax.set_yticks([0.01, 0.1, 1, 10, 100])
    ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
    ax.set_xlim([0, 90])
    
    # Add grid
    ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
    
    # Add reference lines
    ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5, 
               alpha=0.8, label='N2 = N1')
    ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    
    # Add colorbar for period variability
    cbar = plt.colorbar(im, ax=ax, pad=0.02, aspect=30)
    cbar.set_label('Relative Std. Dev. of Period\n(Lower = More Periodic)', 
                   fontsize=12, fontweight='bold')
    
    # Add legend for collision regions
    from matplotlib.patches import Patch
    legend_elements = [
        Patch(facecolor='#ff0000', label='Collision'),
        Patch(facecolor='#440154', label='Low Variability (Stable)'),
        Patch(facecolor='#fde724', label='High Variability (Chaotic)')
    ]
    ax.legend(handles=legend_elements, loc='upper left', fontsize=10,
             framealpha=0.9)
    
    plt.tight_layout()
    plt.savefig('phase_space_heatmap_quantitative.png', dpi=300, bbox_inches='tight')
    print("Heat map saved as 'phase_space_heatmap_quantitative.png'")
    plt.show()
    
    return fig

# MAIN CODE

# Run phase space scan
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

results_grid, theta_range, n2_range = scan_phase_space_2d_quantitative(n_theta=400, n_n2=500)

# Plot results
plot_phase_space_heatmap(results_grid, theta_range, n2_range)

# Save results
np.savez('phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 400 points from 0° to 90°
N2 axis: 500 points from N1/100 to 100*N1
  N2/N1 range: 0.010 to 100.0
Total simulations: 200000
======================================================================


N2/N1 = 0.010 (1/500)
  Progress: 400/200000 (0.2%) | Rate: 42.3 sim/s | ETA: 4720s
N2/N1 = 0.010 (2/500)
  Progress: 800/200000 (0.4%) | Rate: 42.2 sim/s | ETA: 4717s
N2/N1 = 0.010 (3/500)
  Progress: 1200/200000 (0.6%) | Rate: 42.3 sim/s | ETA: 4697s
N2/N1 = 0.011 (4/500)
  Progress: 1600/200000 (0.8%) | Rate: 42.3 sim/s | ETA: 4693s
N2/N1 = 0.011 (5/500)
  Progress: 2000/200000 (1.0%) | Rate: 42.2 sim/s | ETA: 4689s
N2/N1 = 0.011 (6/500)
  Progress: 2400/200000 (1.2%) | Rate: 42.2 sim/s | ETA: 4684s
N2/N1 = 0.011 (7/500)
  Progress: 2800/200000 (1.4%) | Rate: 42.2 sim/s | ETA: 4676s
N2/N1 = 0.011 (8/500)
  Progress: 3200/200000 (1.6%) | Rate: 42.2 sim/s | ETA: 4665s
N2/N1 = 0.012 (9/500)
  Progress: 3600/200000 (1.8%) | Rate: 42.1 sim/s | ETA: 4663s
N2/N1 = 0.012 (10/500)
  Progress: 4000/200000 (2.0%) | Rate: 42.1 sim/s | ETA: 4656s
N2/N1 = 0.012 (11/500)
  Progress: 4400/200000 (2.2%) | Rate: 42.0 sim/s | ETA: 4652s
N2/N1 = 0.012 (12/500)
  Progress: 4800/200000 (2.4%) | Rate: 42.0 sim/s | ETA: 4648s
N2/N1 = 0.012 (13/500)
  Progress: 5200/200000 (2.6%) | Rate: 42.0 sim/s | ETA: 4641s
N2/N1 = 0.013 (14/500)
  Progress: 5600/200000 (2.8%) | Rate: 41.9 sim/s | ETA: 4635s
N2/N1 = 0.013 (15/500)
  Progress: 6000/200000 (3.0%) | Rate: 41.9 sim/s | ETA: 4627s
N2/N1 = 0.013 (16/500)
  Progress: 6400/200000 (3.2%) | Rate: 41.9 sim/s | ETA: 4617s
N2/N1 = 0.013 (17/500)
  Progress: 6800/200000 (3.4%) | Rate: 41.9 sim/s | ETA: 4611s
N2/N1 = 0.014 (18/500)
  Progress: 7200/200000 (3.6%) | Rate: 41.9 sim/s | ETA: 4603s
N2/N1 = 0.014 (19/500)
  Progress: 7600/200000 (3.8%) | Rate: 41.9 sim/s | ETA: 4596s
N2/N1 = 0.014 (20/500)
  Progress: 8000/200000 (4.0%) | Rate: 41.8 sim/s | ETA: 4589s
N2/N1 = 0.014 (21/500)
  Progress: 8400/200000 (4.2%) | Rate: 41.8 sim/s | ETA: 4583s
N2/N1 = 0.015 (22/500)
  Progress: 8800/200000 (4.4%) | Rate: 41.8 sim/s | ETA: 4575s
N2/N1 = 0.015 (23/500)
  Progress: 9200/200000 (4.6%) | Rate: 41.8 sim/s | ETA: 4568s
N2/N1 = 0.015 (24/500)
  Progress: 9600/200000 (4.8%) | Rate: 41.7 sim/s | ETA: 4561s
N2/N1 = 0.016 (25/500)
  Progress: 10000/200000 (5.0%) | Rate: 41.7 sim/s | ETA: 4555s
N2/N1 = 0.016 (26/500)
  Progress: 10400/200000 (5.2%) | Rate: 41.7 sim/s | ETA: 4548s
N2/N1 = 0.016 (27/500)
  Progress: 10800/200000 (5.4%) | Rate: 41.7 sim/s | ETA: 4541s
N2/N1 = 0.016 (28/500)
  Progress: 11200/200000 (5.6%) | Rate: 41.7 sim/s | ETA: 4533s
N2/N1 = 0.017 (29/500)
  Progress: 11600/200000 (5.8%) | Rate: 41.6 sim/s | ETA: 4526s
N2/N1 = 0.017 (30/500)
  Progress: 12000/200000 (6.0%) | Rate: 41.6 sim/s | ETA: 4518s
N2/N1 = 0.017 (31/500)
  Progress: 12400/200000 (6.2%) | Rate: 41.6 sim/s | ETA: 4511s
N2/N1 = 0.018 (32/500)
  Progress: 12800/200000 (6.4%) | Rate: 41.6 sim/s | ETA: 4504s
N2/N1 = 0.018 (33/500)
  Progress: 13200/200000 (6.6%) | Rate: 41.5 sim/s | ETA: 4497s
N2/N1 = 0.018 (34/500)
  Progress: 13600/200000 (6.8%) | Rate: 41.5 sim/s | ETA: 4491s
N2/N1 = 0.019 (35/500)
  Progress: 14000/200000 (7.0%) | Rate: 41.5 sim/s | ETA: 4484s
N2/N1 = 0.019 (36/500)
  Progress: 14400/200000 (7.2%) | Rate: 41.4 sim/s | ETA: 4478s
N2/N1 = 0.019 (37/500)
  Progress: 14800/200000 (7.4%) | Rate: 41.4 sim/s | ETA: 4471s
N2/N1 = 0.020 (38/500)
  Progress: 15200/200000 (7.6%) | Rate: 41.4 sim/s | ETA: 4465s
N2/N1 = 0.020 (39/500)
  Progress: 15600/200000 (7.8%) | Rate: 41.3 sim/s | ETA: 4464s
N2/N1 = 0.021 (40/500)
  Progress: 16000/200000 (8.0%) | Rate: 41.3 sim/s | ETA: 4457s
N2/N1 = 0.021 (41/500)
  Progress: 16400/200000 (8.2%) | Rate: 41.2 sim/s | ETA: 4457s
N2/N1 = 0.021 (42/500)
  Progress: 16800/200000 (8.4%) | Rate: 41.1 sim/s | ETA: 4455s
N2/N1 = 0.022 (43/500)
  Progress: 17200/200000 (8.6%) | Rate: 41.1 sim/s | ETA: 4449s
N2/N1 = 0.022 (44/500)
  Progress: 17600/200000 (8.8%) | Rate: 41.0 sim/s | ETA: 4444s
N2/N1 = 0.023 (45/500)
  Progress: 18000/200000 (9.0%) | Rate: 41.0 sim/s | ETA: 4438s
N2/N1 = 0.023 (46/500)
  Progress: 18400/200000 (9.2%) | Rate: 41.0 sim/s | ETA: 4431s
N2/N1 = 0.023 (47/500)
  Progress: 18800/200000 (9.4%) | Rate: 41.0 sim/s | ETA: 4424s
N2/N1 = 0.024 (48/500)
  Progress: 19200/200000 (9.6%) | Rate: 40.9 sim/s | ETA: 4418s
N2/N1 = 0.024 (49/500)
  Progress: 19600/200000 (9.8%) | Rate: 40.9 sim/s | ETA: 4412s
N2/N1 = 0.025 (50/500)
  Progress: 20000/200000 (10.0%) | Rate: 40.9 sim/s | ETA: 4406s
N2/N1 = 0.025 (51/500)
  Progress: 20400/200000 (10.2%) | Rate: 40.8 sim/s | ETA: 4400s
N2/N1 = 0.026 (52/500)
  Progress: 20800/200000 (10.4%) | Rate: 40.8 sim/s | ETA: 4394s
N2/N1 = 0.026 (53/500)
  Progress: 21200/200000 (10.6%) | Rate: 40.8 sim/s | ETA: 4387s
N2/N1 = 0.027 (54/500)
  Progress: 21600/200000 (10.8%) | Rate: 40.7 sim/s | ETA: 4381s
N2/N1 = 0.027 (55/500)
  Progress: 22000/200000 (11.0%) | Rate: 40.7 sim/s | ETA: 4375s
N2/N1 = 0.028 (56/500)
  Progress: 22400/200000 (11.2%) | Rate: 40.6 sim/s | ETA: 4369s
N2/N1 = 0.028 (57/500)
  Progress: 22800/200000 (11.4%) | Rate: 40.6 sim/s | ETA: 4364s
N2/N1 = 0.029 (58/500)
  Progress: 23200/200000 (11.6%) | Rate: 40.6 sim/s | ETA: 4359s
N2/N1 = 0.029 (59/500)
  Progress: 23600/200000 (11.8%) | Rate: 40.5 sim/s | ETA: 4353s
N2/N1 = 0.030 (60/500)
  Progress: 24000/200000 (12.0%) | Rate: 40.5 sim/s | ETA: 4347s
N2/N1 = 0.030 (61/500)
  Progress: 24400/200000 (12.2%) | Rate: 40.4 sim/s | ETA: 4342s
N2/N1 = 0.031 (62/500)
  Progress: 24800/200000 (12.4%) | Rate: 40.4 sim/s | ETA: 4336s
N2/N1 = 0.031 (63/500)
  Progress: 25200/200000 (12.6%) | Rate: 40.4 sim/s | ETA: 4330s
N2/N1 = 0.032 (64/500)
  Progress: 25600/200000 (12.8%) | Rate: 40.3 sim/s | ETA: 4324s
N2/N1 = 0.033 (65/500)
  Progress: 26000/200000 (13.0%) | Rate: 40.3 sim/s | ETA: 4318s
N2/N1 = 0.033 (66/500)
  Progress: 26400/200000 (13.2%) | Rate: 40.3 sim/s | ETA: 4312s
N2/N1 = 0.034 (67/500)
  Progress: 26800/200000 (13.4%) | Rate: 40.2 sim/s | ETA: 4308s
N2/N1 = 0.034 (68/500)
  Progress: 27200/200000 (13.6%) | Rate: 40.2 sim/s | ETA: 4302s
N2/N1 = 0.035 (69/500)
  Progress: 27600/200000 (13.8%) | Rate: 40.1 sim/s | ETA: 4297s
N2/N1 = 0.036 (70/500)
  Progress: 28000/200000 (14.0%) | Rate: 40.1 sim/s | ETA: 4292s
N2/N1 = 0.036 (71/500)
  Progress: 28400/200000 (14.2%) | Rate: 40.0 sim/s | ETA: 4287s
N2/N1 = 0.037 (72/500)
  Progress: 28800/200000 (14.4%) | Rate: 40.0 sim/s | ETA: 4281s
N2/N1 = 0.038 (73/500)
  Progress: 29200/200000 (14.6%) | Rate: 40.0 sim/s | ETA: 4275s
N2/N1 = 0.038 (74/500)
  Progress: 29600/200000 (14.8%) | Rate: 39.9 sim/s | ETA: 4269s
N2/N1 = 0.039 (75/500)
  Progress: 30000/200000 (15.0%) | Rate: 39.9 sim/s | ETA: 4264s
N2/N1 = 0.040 (76/500)
  Progress: 30400/200000 (15.2%) | Rate: 39.8 sim/s | ETA: 4258s
N2/N1 = 0.041 (77/500)
  Progress: 30800/200000 (15.4%) | Rate: 39.8 sim/s | ETA: 4253s
N2/N1 = 0.041 (78/500)
  Progress: 31200/200000 (15.6%) | Rate: 39.7 sim/s | ETA: 4248s
N2/N1 = 0.042 (79/500)
  Progress: 31600/200000 (15.8%) | Rate: 39.7 sim/s | ETA: 4243s
N2/N1 = 0.043 (80/500)
  Progress: 32000/200000 (16.0%) | Rate: 39.6 sim/s | ETA: 4237s
N2/N1 = 0.044 (81/500)
  Progress: 32400/200000 (16.2%) | Rate: 39.6 sim/s | ETA: 4233s
N2/N1 = 0.045 (82/500)
  Progress: 32800/200000 (16.4%) | Rate: 39.5 sim/s | ETA: 4228s
N2/N1 = 0.045 (83/500)
  Progress: 33200/200000 (16.6%) | Rate: 39.5 sim/s | ETA: 4224s
N2/N1 = 0.046 (84/500)
  Progress: 33600/200000 (16.8%) | Rate: 39.4 sim/s | ETA: 4219s
N2/N1 = 0.047 (85/500)
  Progress: 34000/200000 (17.0%) | Rate: 39.4 sim/s | ETA: 4216s
N2/N1 = 0.048 (86/500)
  Progress: 34400/200000 (17.2%) | Rate: 39.3 sim/s | ETA: 4212s
N2/N1 = 0.049 (87/500)
  Progress: 34800/200000 (17.4%) | Rate: 39.3 sim/s | ETA: 4209s
N2/N1 = 0.050 (88/500)
  Progress: 35200/200000 (17.6%) | Rate: 39.2 sim/s | ETA: 4206s
N2/N1 = 0.051 (89/500)
  Progress: 35600/200000 (17.8%) | Rate: 39.1 sim/s | ETA: 4203s
N2/N1 = 0.052 (90/500)
  Progress: 36000/200000 (18.0%) | Rate: 39.0 sim/s | ETA: 4201s
N2/N1 = 0.053 (91/500)
  Progress: 36400/200000 (18.2%) | Rate: 39.0 sim/s | ETA: 4200s
N2/N1 = 0.054 (92/500)
  Progress: 36800/200000 (18.4%) | Rate: 38.9 sim/s | ETA: 4200s
N2/N1 = 0.055 (93/500)
  Progress: 37200/200000 (18.6%) | Rate: 38.8 sim/s | ETA: 4200s
N2/N1 = 0.056 (94/500)
  Progress: 37600/200000 (18.8%) | Rate: 38.6 sim/s | ETA: 4202s
N2/N1 = 0.057 (95/500)
  Progress: 38000/200000 (19.0%) | Rate: 38.5 sim/s | ETA: 4205s
N2/N1 = 0.058 (96/500)
  Progress: 38400/200000 (19.2%) | Rate: 38.4 sim/s | ETA: 4208s
N2/N1 = 0.059 (97/500)
  Progress: 38800/200000 (19.4%) | Rate: 38.3 sim/s | ETA: 4212s
N2/N1 = 0.060 (98/500)
  Progress: 39200/200000 (19.6%) | Rate: 38.1 sim/s | ETA: 4216s
N2/N1 = 0.061 (99/500)
  Progress: 39600/200000 (19.8%) | Rate: 38.0 sim/s | ETA: 4220s
N2/N1 = 0.062 (100/500)
  Progress: 40000/200000 (20.0%) | Rate: 37.9 sim/s | ETA: 4224s
N2/N1 = 0.063 (101/500)
  Progress: 40400/200000 (20.2%) | Rate: 37.7 sim/s | ETA: 4228s
N2/N1 = 0.065 (102/500)
  Progress: 40800/200000 (20.4%) | Rate: 37.6 sim/s | ETA: 4232s
N2/N1 = 0.066 (103/500)
  Progress: 41200/200000 (20.6%) | Rate: 37.5 sim/s | ETA: 4236s
N2/N1 = 0.067 (104/500)
  Progress: 41600/200000 (20.8%) | Rate: 37.4 sim/s | ETA: 4240s
N2/N1 = 0.068 (105/500)
  Progress: 42000/200000 (21.0%) | Rate: 37.2 sim/s | ETA: 4244s
N2/N1 = 0.069 (106/500)
  Progress: 42400/200000 (21.2%) | Rate: 37.1 sim/s | ETA: 4248s
N2/N1 = 0.071 (107/500)
  Progress: 42800/200000 (21.4%) | Rate: 37.0 sim/s | ETA: 4252s
N2/N1 = 0.072 (108/500)
  Progress: 43200/200000 (21.6%) | Rate: 36.8 sim/s | ETA: 4257s
N2/N1 = 0.073 (109/500)
  Progress: 43600/200000 (21.8%) | Rate: 36.7 sim/s | ETA: 4261s
N2/N1 = 0.075 (110/500)
  Progress: 44000/200000 (22.0%) | Rate: 36.6 sim/s | ETA: 4264s
N2/N1 = 0.076 (111/500)
  Progress: 44400/200000 (22.2%) | Rate: 36.5 sim/s | ETA: 4268s
N2/N1 = 0.078 (112/500)
  Progress: 44800/200000 (22.4%) | Rate: 36.3 sim/s | ETA: 4272s
N2/N1 = 0.079 (113/500)
  Progress: 45200/200000 (22.6%) | Rate: 36.2 sim/s | ETA: 4276s
N2/N1 = 0.081 (114/500)
  Progress: 45600/200000 (22.8%) | Rate: 36.1 sim/s | ETA: 4279s
N2/N1 = 0.082 (115/500)
  Progress: 46000/200000 (23.0%) | Rate: 36.0 sim/s | ETA: 4283s
N2/N1 = 0.084 (116/500)
  Progress: 46400/200000 (23.2%) | Rate: 35.8 sim/s | ETA: 4287s
N2/N1 = 0.085 (117/500)
  Progress: 46800/200000 (23.4%) | Rate: 35.7 sim/s | ETA: 4290s
N2/N1 = 0.087 (118/500)
  Progress: 47200/200000 (23.6%) | Rate: 35.6 sim/s | ETA: 4294s
N2/N1 = 0.088 (119/500)
  Progress: 47600/200000 (23.8%) | Rate: 35.5 sim/s | ETA: 4298s
N2/N1 = 0.090 (120/500)
  Progress: 48000/200000 (24.0%) | Rate: 35.3 sim/s | ETA: 4301s
N2/N1 = 0.092 (121/500)
  Progress: 48400/200000 (24.2%) | Rate: 35.2 sim/s | ETA: 4305s
N2/N1 = 0.093 (122/500)
  Progress: 48800/200000 (24.4%) | Rate: 35.1 sim/s | ETA: 4309s
N2/N1 = 0.095 (123/500)
  Progress: 49200/200000 (24.6%) | Rate: 35.0 sim/s | ETA: 4313s
N2/N1 = 0.097 (124/500)
  Progress: 49600/200000 (24.8%) | Rate: 34.8 sim/s | ETA: 4317s
N2/N1 = 0.099 (125/500)
  Progress: 50000/200000 (25.0%) | Rate: 34.7 sim/s | ETA: 4321s
N2/N1 = 0.100 (126/500)
  Progress: 50400/200000 (25.2%) | Rate: 34.6 sim/s | ETA: 4326s
N2/N1 = 0.102 (127/500)
  Progress: 50800/200000 (25.4%) | Rate: 34.5 sim/s | ETA: 4331s
N2/N1 = 0.104 (128/500)
  Progress: 51200/200000 (25.6%) | Rate: 34.3 sim/s | ETA: 4335s
N2/N1 = 0.106 (129/500)
  Progress: 51600/200000 (25.8%) | Rate: 34.2 sim/s | ETA: 4340s
N2/N1 = 0.108 (130/500)
  Progress: 52000/200000 (26.0%) | Rate: 34.1 sim/s | ETA: 4346s
N2/N1 = 0.110 (131/500)
  Progress: 52400/200000 (26.2%) | Rate: 33.9 sim/s | ETA: 4351s
N2/N1 = 0.112 (132/500)
  Progress: 52800/200000 (26.4%) | Rate: 33.8 sim/s | ETA: 4357s
N2/N1 = 0.114 (133/500)
  Progress: 53200/200000 (26.6%) | Rate: 33.6 sim/s | ETA: 4363s
N2/N1 = 0.116 (134/500)
  Progress: 53600/200000 (26.8%) | Rate: 33.5 sim/s | ETA: 4369s
N2/N1 = 0.119 (135/500)
  Progress: 54000/200000 (27.0%) | Rate: 33.4 sim/s | ETA: 4375s
N2/N1 = 0.121 (136/500)
  Progress: 54400/200000 (27.2%) | Rate: 33.2 sim/s | ETA: 4383s
N2/N1 = 0.123 (137/500)
  Progress: 54800/200000 (27.4%) | Rate: 33.1 sim/s | ETA: 4390s
N2/N1 = 0.125 (138/500)
  Progress: 55200/200000 (27.6%) | Rate: 32.9 sim/s | ETA: 4397s
N2/N1 = 0.128 (139/500)
  Progress: 55600/200000 (27.8%) | Rate: 32.8 sim/s | ETA: 4404s
N2/N1 = 0.130 (140/500)
  Progress: 56000/200000 (28.0%) | Rate: 32.6 sim/s | ETA: 4412s
N2/N1 = 0.133 (141/500)
  Progress: 56400/200000 (28.2%) | Rate: 32.5 sim/s | ETA: 4419s
N2/N1 = 0.135 (142/500)
  Progress: 56800/200000 (28.4%) | Rate: 32.3 sim/s | ETA: 4427s
N2/N1 = 0.137 (143/500)
  Progress: 57200/200000 (28.6%) | Rate: 32.2 sim/s | ETA: 4434s
N2/N1 = 0.140 (144/500)
  Progress: 57600/200000 (28.8%) | Rate: 32.1 sim/s | ETA: 4442s
N2/N1 = 0.143 (145/500)
  Progress: 58000/200000 (29.0%) | Rate: 31.9 sim/s | ETA: 4449s
N2/N1 = 0.145 (146/500)
  Progress: 58400/200000 (29.2%) | Rate: 31.8 sim/s | ETA: 4457s
N2/N1 = 0.148 (147/500)
  Progress: 58800/200000 (29.4%) | Rate: 31.6 sim/s | ETA: 4464s
N2/N1 = 0.151 (148/500)
  Progress: 59200/200000 (29.6%) | Rate: 31.5 sim/s | ETA: 4471s
N2/N1 = 0.154 (149/500)
  Progress: 59600/200000 (29.8%) | Rate: 31.4 sim/s | ETA: 4477s
N2/N1 = 0.156 (150/500)
  Progress: 60000/200000 (30.0%) | Rate: 31.2 sim/s | ETA: 4485s
N2/N1 = 0.159 (151/500)
  Progress: 60400/200000 (30.2%) | Rate: 31.1 sim/s | ETA: 4492s
N2/N1 = 0.162 (152/500)
  Progress: 60800/200000 (30.4%) | Rate: 30.9 sim/s | ETA: 4499s
N2/N1 = 0.165 (153/500)
  Progress: 61200/200000 (30.6%) | Rate: 30.8 sim/s | ETA: 4506s
N2/N1 = 0.168 (154/500)
  Progress: 61600/200000 (30.8%) | Rate: 30.7 sim/s | ETA: 4513s
N2/N1 = 0.172 (155/500)
  Progress: 62000/200000 (31.0%) | Rate: 30.5 sim/s | ETA: 4519s
N2/N1 = 0.175 (156/500)
  Progress: 62400/200000 (31.2%) | Rate: 30.4 sim/s | ETA: 4525s
N2/N1 = 0.178 (157/500)
  Progress: 62800/200000 (31.4%) | Rate: 30.3 sim/s | ETA: 4531s
N2/N1 = 0.181 (158/500)
  Progress: 63200/200000 (31.6%) | Rate: 30.1 sim/s | ETA: 4538s
N2/N1 = 0.185 (159/500)
  Progress: 63600/200000 (31.8%) | Rate: 30.0 sim/s | ETA: 4544s
N2/N1 = 0.188 (160/500)
  Progress: 64000/200000 (32.0%) | Rate: 29.9 sim/s | ETA: 4551s
N2/N1 = 0.192 (161/500)
  Progress: 64400/200000 (32.2%) | Rate: 29.8 sim/s | ETA: 4558s
N2/N1 = 0.195 (162/500)
  Progress: 64800/200000 (32.4%) | Rate: 29.6 sim/s | ETA: 4565s
N2/N1 = 0.199 (163/500)
  Progress: 65200/200000 (32.6%) | Rate: 29.5 sim/s | ETA: 4572s
N2/N1 = 0.203 (164/500)
  Progress: 65600/200000 (32.8%) | Rate: 29.4 sim/s | ETA: 4579s
N2/N1 = 0.206 (165/500)
  Progress: 66000/200000 (33.0%) | Rate: 29.2 sim/s | ETA: 4586s
N2/N1 = 0.210 (166/500)
  Progress: 66400/200000 (33.2%) | Rate: 29.1 sim/s | ETA: 4593s
N2/N1 = 0.214 (167/500)
  Progress: 66800/200000 (33.4%) | Rate: 29.0 sim/s | ETA: 4600s
N2/N1 = 0.218 (168/500)
  Progress: 67200/200000 (33.6%) | Rate: 28.8 sim/s | ETA: 4608s
N2/N1 = 0.222 (169/500)
  Progress: 67600/200000 (33.8%) | Rate: 28.7 sim/s | ETA: 4616s
N2/N1 = 0.226 (170/500)
  Progress: 68000/200000 (34.0%) | Rate: 28.5 sim/s | ETA: 4624s
N2/N1 = 0.231 (171/500)
  Progress: 68400/200000 (34.2%) | Rate: 28.4 sim/s | ETA: 4633s
N2/N1 = 0.235 (172/500)
  Progress: 68800/200000 (34.4%) | Rate: 28.3 sim/s | ETA: 4642s
N2/N1 = 0.239 (173/500)
  Progress: 69200/200000 (34.6%) | Rate: 28.1 sim/s | ETA: 4650s
N2/N1 = 0.244 (174/500)
  Progress: 69600/200000 (34.8%) | Rate: 28.0 sim/s | ETA: 4659s
N2/N1 = 0.248 (175/500)
  Progress: 70000/200000 (35.0%) | Rate: 27.8 sim/s | ETA: 4670s
N2/N1 = 0.253 (176/500)
  Progress: 70400/200000 (35.2%) | Rate: 27.7 sim/s | ETA: 4680s
N2/N1 = 0.258 (177/500)
  Progress: 70800/200000 (35.4%) | Rate: 27.5 sim/s | ETA: 4691s
N2/N1 = 0.262 (178/500)
  Progress: 71200/200000 (35.6%) | Rate: 27.4 sim/s | ETA: 4702s
N2/N1 = 0.267 (179/500)
  Progress: 71600/200000 (35.8%) | Rate: 27.2 sim/s | ETA: 4714s
N2/N1 = 0.272 (180/500)
  Progress: 72000/200000 (36.0%) | Rate: 27.1 sim/s | ETA: 4726s
N2/N1 = 0.277 (181/500)
  Progress: 72400/200000 (36.2%) | Rate: 26.9 sim/s | ETA: 4737s
N2/N1 = 0.282 (182/500)
  Progress: 72800/200000 (36.4%) | Rate: 26.8 sim/s | ETA: 4749s
N2/N1 = 0.288 (183/500)
  Progress: 73200/200000 (36.6%) | Rate: 26.6 sim/s | ETA: 4760s
N2/N1 = 0.293 (184/500)
  Progress: 73600/200000 (36.8%) | Rate: 26.5 sim/s | ETA: 4772s
N2/N1 = 0.299 (185/500)
  Progress: 74000/200000 (37.0%) | Rate: 26.3 sim/s | ETA: 4783s
N2/N1 = 0.304 (186/500)
  Progress: 74400/200000 (37.2%) | Rate: 26.2 sim/s | ETA: 4794s
N2/N1 = 0.310 (187/500)
  Progress: 74800/200000 (37.4%) | Rate: 26.1 sim/s | ETA: 4805s
N2/N1 = 0.315 (188/500)
  Progress: 75200/200000 (37.6%) | Rate: 25.9 sim/s | ETA: 4816s
N2/N1 = 0.321 (189/500)
  Progress: 75600/200000 (37.8%) | Rate: 25.8 sim/s | ETA: 4828s
N2/N1 = 0.327 (190/500)
  Progress: 76000/200000 (38.0%) | Rate: 25.6 sim/s | ETA: 4840s
N2/N1 = 0.333 (191/500)
  Progress: 76400/200000 (38.2%) | Rate: 25.5 sim/s | ETA: 4852s
N2/N1 = 0.340 (192/500)
  Progress: 76800/200000 (38.4%) | Rate: 25.3 sim/s | ETA: 4865s
N2/N1 = 0.346 (193/500)
  Progress: 77200/200000 (38.6%) | Rate: 25.2 sim/s | ETA: 4878s
N2/N1 = 0.352 (194/500)
  Progress: 77600/200000 (38.8%) | Rate: 25.0 sim/s | ETA: 4891s
N2/N1 = 0.359 (195/500)
  Progress: 78000/200000 (39.0%) | Rate: 24.9 sim/s | ETA: 4905s
N2/N1 = 0.366 (196/500)
  Progress: 78400/200000 (39.2%) | Rate: 24.7 sim/s | ETA: 4919s
N2/N1 = 0.373 (197/500)
  Progress: 78800/200000 (39.4%) | Rate: 24.6 sim/s | ETA: 4933s
N2/N1 = 0.379 (198/500)
  Progress: 79200/200000 (39.6%) | Rate: 24.4 sim/s | ETA: 4947s
N2/N1 = 0.387 (199/500)
  Progress: 79600/200000 (39.8%) | Rate: 24.3 sim/s | ETA: 4962s
N2/N1 = 0.394 (200/500)
  Progress: 80000/200000 (40.0%) | Rate: 24.1 sim/s | ETA: 4977s
N2/N1 = 0.401 (201/500)
  Progress: 80400/200000 (40.2%) | Rate: 24.0 sim/s | ETA: 4993s
N2/N1 = 0.409 (202/500)
  Progress: 80800/200000 (40.4%) | Rate: 23.8 sim/s | ETA: 5008s
N2/N1 = 0.416 (203/500)
  Progress: 81200/200000 (40.6%) | Rate: 23.6 sim/s | ETA: 5024s
N2/N1 = 0.424 (204/500)
  Progress: 81600/200000 (40.8%) | Rate: 23.5 sim/s | ETA: 5040s
N2/N1 = 0.432 (205/500)
  Progress: 82000/200000 (41.0%) | Rate: 23.3 sim/s | ETA: 5056s
N2/N1 = 0.440 (206/500)
  Progress: 82400/200000 (41.2%) | Rate: 23.2 sim/s | ETA: 5072s
N2/N1 = 0.448 (207/500)
  Progress: 82800/200000 (41.4%) | Rate: 23.0 sim/s | ETA: 5089s
N2/N1 = 0.456 (208/500)
  Progress: 83200/200000 (41.6%) | Rate: 22.9 sim/s | ETA: 5108s
N2/N1 = 0.465 (209/500)
  Progress: 83600/200000 (41.8%) | Rate: 22.7 sim/s | ETA: 5127s
N2/N1 = 0.474 (210/500)
  Progress: 84000/200000 (42.0%) | Rate: 22.5 sim/s | ETA: 5146s
N2/N1 = 0.482 (211/500)
  Progress: 84400/200000 (42.2%) | Rate: 22.4 sim/s | ETA: 5165s
N2/N1 = 0.491 (212/500)
  Progress: 84800/200000 (42.4%) | Rate: 22.2 sim/s | ETA: 5185s
N2/N1 = 0.500 (213/500)
  Progress: 85200/200000 (42.6%) | Rate: 22.1 sim/s | ETA: 5204s
N2/N1 = 0.510 (214/500)
  Progress: 85600/200000 (42.8%) | Rate: 21.9 sim/s | ETA: 5224s
N2/N1 = 0.519 (215/500)
  Progress: 86000/200000 (43.0%) | Rate: 21.7 sim/s | ETA: 5245s
N2/N1 = 0.529 (216/500)
  Progress: 86400/200000 (43.2%) | Rate: 21.6 sim/s | ETA: 5266s
N2/N1 = 0.539 (217/500)
  Progress: 86800/200000 (43.4%) | Rate: 21.4 sim/s | ETA: 5288s
N2/N1 = 0.549 (218/500)
  Progress: 87200/200000 (43.6%) | Rate: 21.2 sim/s | ETA: 5310s
N2/N1 = 0.559 (219/500)
  Progress: 87600/200000 (43.8%) | Rate: 21.1 sim/s | ETA: 5332s
N2/N1 = 0.570 (220/500)
  Progress: 88000/200000 (44.0%) | Rate: 20.9 sim/s | ETA: 5355s
N2/N1 = 0.580 (221/500)
  Progress: 88400/200000 (44.2%) | Rate: 20.7 sim/s | ETA: 5379s
N2/N1 = 0.591 (222/500)
  Progress: 88800/200000 (44.4%) | Rate: 20.6 sim/s | ETA: 5403s
N2/N1 = 0.602 (223/500)
  Progress: 89200/200000 (44.6%) | Rate: 20.4 sim/s | ETA: 5427s
N2/N1 = 0.613 (224/500)
  Progress: 89600/200000 (44.8%) | Rate: 20.2 sim/s | ETA: 5452s
N2/N1 = 0.625 (225/500)
  Progress: 90000/200000 (45.0%) | Rate: 20.1 sim/s | ETA: 5478s
N2/N1 = 0.636 (226/500)
  Progress: 90400/200000 (45.2%) | Rate: 19.9 sim/s | ETA: 5505s
N2/N1 = 0.648 (227/500)
  Progress: 90800/200000 (45.4%) | Rate: 19.7 sim/s | ETA: 5532s
N2/N1 = 0.660 (228/500)
  Progress: 91200/200000 (45.6%) | Rate: 19.6 sim/s | ETA: 5559s
N2/N1 = 0.672 (229/500)
  Progress: 91600/200000 (45.8%) | Rate: 19.4 sim/s | ETA: 5587s
N2/N1 = 0.685 (230/500)
  Progress: 92000/200000 (46.0%) | Rate: 19.2 sim/s | ETA: 5616s
N2/N1 = 0.698 (231/500)
  Progress: 92400/200000 (46.2%) | Rate: 19.1 sim/s | ETA: 5645s
N2/N1 = 0.711 (232/500)
  Progress: 92800/200000 (46.4%) | Rate: 18.9 sim/s | ETA: 5676s
N2/N1 = 0.724 (233/500)
  Progress: 93200/200000 (46.6%) | Rate: 18.7 sim/s | ETA: 5706s
N2/N1 = 0.737 (234/500)
  Progress: 93600/200000 (46.8%) | Rate: 18.5 sim/s | ETA: 5738s
N2/N1 = 0.751 (235/500)
  Progress: 94000/200000 (47.0%) | Rate: 18.4 sim/s | ETA: 5772s
N2/N1 = 0.765 (236/500)
  Progress: 94400/200000 (47.2%) | Rate: 18.2 sim/s | ETA: 5805s
N2/N1 = 0.779 (237/500)
  Progress: 94800/200000 (47.4%) | Rate: 18.0 sim/s | ETA: 5837s
N2/N1 = 0.794 (238/500)
  Progress: 95200/200000 (47.6%) | Rate: 17.9 sim/s | ETA: 5868s
N2/N1 = 0.809 (239/500)
  Progress: 95600/200000 (47.8%) | Rate: 17.7 sim/s | ETA: 5896s
N2/N1 = 0.824 (240/500)
  Progress: 96000/200000 (48.0%) | Rate: 17.6 sim/s | ETA: 5925s
N2/N1 = 0.839 (241/500)
  Progress: 96400/200000 (48.2%) | Rate: 17.4 sim/s | ETA: 5953s
N2/N1 = 0.855 (242/500)
  Progress: 96800/200000 (48.4%) | Rate: 17.3 sim/s | ETA: 5980s
N2/N1 = 0.871 (243/500)
  Progress: 97200/200000 (48.6%) | Rate: 17.1 sim/s | ETA: 6007s
N2/N1 = 0.887 (244/500)
  Progress: 97600/200000 (48.8%) | Rate: 17.0 sim/s | ETA: 6036s
N2/N1 = 0.903 (245/500)
  Progress: 98000/200000 (49.0%) | Rate: 16.8 sim/s | ETA: 6062s
N2/N1 = 0.920 (246/500)
  Progress: 98400/200000 (49.2%) | Rate: 16.7 sim/s | ETA: 6087s
N2/N1 = 0.937 (247/500)
  Progress: 98800/200000 (49.4%) | Rate: 16.6 sim/s | ETA: 6113s
N2/N1 = 0.955 (248/500)
  Progress: 99200/200000 (49.6%) | Rate: 16.4 sim/s | ETA: 6138s
N2/N1 = 0.973 (249/500)
  Progress: 99600/200000 (49.8%) | Rate: 16.3 sim/s | ETA: 6163s
N2/N1 = 0.991 (250/500)
  Progress: 100000/200000 (50.0%) | Rate: 16.2 sim/s | ETA: 6188s
N2/N1 = 1.009 (251/500)
  Progress: 100400/200000 (50.2%) | Rate: 16.0 sim/s | ETA: 6213s
N2/N1 = 1.028 (252/500)
  Progress: 100800/200000 (50.4%) | Rate: 15.9 sim/s | ETA: 6236s
N2/N1 = 1.047 (253/500)
  Progress: 101200/200000 (50.6%) | Rate: 15.8 sim/s | ETA: 6260s
N2/N1 = 1.067 (254/500)
  Progress: 101600/200000 (50.8%) | Rate: 15.7 sim/s | ETA: 6284s
N2/N1 = 1.087 (255/500)
  Progress: 102000/200000 (51.0%) | Rate: 15.5 sim/s | ETA: 6307s
N2/N1 = 1.107 (256/500)
  Progress: 102400/200000 (51.2%) | Rate: 15.4 sim/s | ETA: 6329s
N2/N1 = 1.127 (257/500)
  Progress: 102800/200000 (51.4%) | Rate: 15.3 sim/s | ETA: 6352s
N2/N1 = 1.148 (258/500)
  Progress: 103200/200000 (51.6%) | Rate: 15.2 sim/s | ETA: 6374s
N2/N1 = 1.170 (259/500)
  Progress: 103600/200000 (51.8%) | Rate: 15.1 sim/s | ETA: 6397s
N2/N1 = 1.192 (260/500)
  Progress: 104000/200000 (52.0%) | Rate: 15.0 sim/s | ETA: 6419s
N2/N1 = 1.214 (261/500)
  Progress: 104400/200000 (52.2%) | Rate: 14.8 sim/s | ETA: 6440s
N2/N1 = 1.236 (262/500)
  Progress: 104800/200000 (52.4%) | Rate: 14.7 sim/s | ETA: 6462s
N2/N1 = 1.260 (263/500)
  Progress: 105200/200000 (52.6%) | Rate: 14.6 sim/s | ETA: 6482s
N2/N1 = 1.283 (264/500)
  Progress: 105600/200000 (52.8%) | Rate: 14.5 sim/s | ETA: 6503s
N2/N1 = 1.307 (265/500)
  Progress: 106000/200000 (53.0%) | Rate: 14.4 sim/s | ETA: 6523s
N2/N1 = 1.331 (266/500)
  Progress: 106400/200000 (53.2%) | Rate: 14.3 sim/s | ETA: 6543s
N2/N1 = 1.356 (267/500)
  Progress: 106800/200000 (53.4%) | Rate: 14.2 sim/s | ETA: 6562s
N2/N1 = 1.381 (268/500)
  Progress: 107200/200000 (53.6%) | Rate: 14.1 sim/s | ETA: 6582s
N2/N1 = 1.407 (269/500)
  Progress: 107600/200000 (53.8%) | Rate: 14.0 sim/s | ETA: 6601s
N2/N1 = 1.433 (270/500)
  Progress: 108000/200000 (54.0%) | Rate: 13.9 sim/s | ETA: 6619s
N2/N1 = 1.460 (271/500)
  Progress: 108400/200000 (54.2%) | Rate: 13.8 sim/s | ETA: 6639s
N2/N1 = 1.487 (272/500)
  Progress: 108800/200000 (54.4%) | Rate: 13.7 sim/s | ETA: 6657s
N2/N1 = 1.515 (273/500)
  Progress: 109200/200000 (54.6%) | Rate: 13.6 sim/s | ETA: 6675s
N2/N1 = 1.543 (274/500)
  Progress: 109600/200000 (54.8%) | Rate: 13.5 sim/s | ETA: 6693s
N2/N1 = 1.572 (275/500)
  Progress: 110000/200000 (55.0%) | Rate: 13.4 sim/s | ETA: 6711s
N2/N1 = 1.601 (276/500)
  Progress: 110400/200000 (55.2%) | Rate: 13.3 sim/s | ETA: 6728s
N2/N1 = 1.631 (277/500)
  Progress: 110800/200000 (55.4%) | Rate: 13.2 sim/s | ETA: 6750s
N2/N1 = 1.661 (278/500)
  Progress: 111200/200000 (55.6%) | Rate: 13.1 sim/s | ETA: 6767s
N2/N1 = 1.692 (279/500)
  Progress: 111600/200000 (55.8%) | Rate: 13.0 sim/s | ETA: 6783s
N2/N1 = 1.724 (280/500)
  Progress: 112000/200000 (56.0%) | Rate: 12.9 sim/s | ETA: 6799s
N2/N1 = 1.756 (281/500)
  Progress: 112400/200000 (56.2%) | Rate: 12.9 sim/s | ETA: 6815s
N2/N1 = 1.789 (282/500)
  Progress: 112800/200000 (56.4%) | Rate: 12.8 sim/s | ETA: 6831s
N2/N1 = 1.822 (283/500)
  Progress: 113200/200000 (56.6%) | Rate: 12.7 sim/s | ETA: 6846s
N2/N1 = 1.856 (284/500)
  Progress: 113600/200000 (56.8%) | Rate: 12.6 sim/s | ETA: 6861s
N2/N1 = 1.890 (285/500)
  Progress: 114000/200000 (57.0%) | Rate: 12.5 sim/s | ETA: 6875s
N2/N1 = 1.926 (286/500)
  Progress: 114400/200000 (57.2%) | Rate: 12.4 sim/s | ETA: 6889s
N2/N1 = 1.961 (287/500)
  Progress: 114800/200000 (57.4%) | Rate: 12.3 sim/s | ETA: 6907s
N2/N1 = 1.998 (288/500)
  Progress: 115200/200000 (57.6%) | Rate: 12.3 sim/s | ETA: 6920s
N2/N1 = 2.035 (289/500)
  Progress: 115600/200000 (57.8%) | Rate: 12.2 sim/s | ETA: 6933s
N2/N1 = 2.073 (290/500)
  Progress: 116000/200000 (58.0%) | Rate: 12.1 sim/s | ETA: 6946s
N2/N1 = 2.112 (291/500)
  Progress: 116400/200000 (58.2%) | Rate: 12.0 sim/s | ETA: 6959s
N2/N1 = 2.151 (292/500)
  Progress: 116800/200000 (58.4%) | Rate: 11.9 sim/s | ETA: 6971s
N2/N1 = 2.191 (293/500)
  Progress: 117200/200000 (58.6%) | Rate: 11.9 sim/s | ETA: 6983s
N2/N1 = 2.232 (294/500)
  Progress: 117600/200000 (58.8%) | Rate: 11.8 sim/s | ETA: 6994s
N2/N1 = 2.274 (295/500)
  Progress: 118000/200000 (59.0%) | Rate: 11.7 sim/s | ETA: 7005s
N2/N1 = 2.316 (296/500)
  Progress: 118400/200000 (59.2%) | Rate: 11.6 sim/s | ETA: 7015s
N2/N1 = 2.359 (297/500)
  Progress: 118800/200000 (59.4%) | Rate: 11.6 sim/s | ETA: 7025s
N2/N1 = 2.403 (298/500)
  Progress: 119200/200000 (59.6%) | Rate: 11.5 sim/s | ETA: 7036s
N2/N1 = 2.448 (299/500)
  Progress: 119600/200000 (59.8%) | Rate: 11.4 sim/s | ETA: 7046s
N2/N1 = 2.493 (300/500)
  Progress: 120000/200000 (60.0%) | Rate: 11.3 sim/s | ETA: 7055s
N2/N1 = 2.540 (301/500)
  Progress: 120400/200000 (60.2%) | Rate: 11.3 sim/s | ETA: 7064s
N2/N1 = 2.587 (302/500)
  Progress: 120800/200000 (60.4%) | Rate: 11.2 sim/s | ETA: 7072s
N2/N1 = 2.635 (303/500)
  Progress: 121200/200000 (60.6%) | Rate: 11.1 sim/s | ETA: 7080s
N2/N1 = 2.684 (304/500)
  Progress: 121600/200000 (60.8%) | Rate: 11.1 sim/s | ETA: 7088s
N2/N1 = 2.734 (305/500)
  Progress: 122000/200000 (61.0%) | Rate: 11.0 sim/s | ETA: 7095s
N2/N1 = 2.785 (306/500)
  Progress: 122400/200000 (61.2%) | Rate: 10.9 sim/s | ETA: 7103s
N2/N1 = 2.837 (307/500)
  Progress: 122800/200000 (61.4%) | Rate: 10.9 sim/s | ETA: 7111s
N2/N1 = 2.890 (308/500)
  Progress: 123200/200000 (61.6%) | Rate: 10.8 sim/s | ETA: 7118s
N2/N1 = 2.944 (309/500)
  Progress: 123600/200000 (61.8%) | Rate: 10.7 sim/s | ETA: 7124s
N2/N1 = 2.999 (310/500)
  Progress: 124000/200000 (62.0%) | Rate: 10.7 sim/s | ETA: 7130s
N2/N1 = 3.055 (311/500)
  Progress: 124400/200000 (62.2%) | Rate: 10.6 sim/s | ETA: 7135s
N2/N1 = 3.112 (312/500)
  Progress: 124800/200000 (62.4%) | Rate: 10.5 sim/s | ETA: 7140s
N2/N1 = 3.170 (313/500)
  Progress: 125200/200000 (62.6%) | Rate: 10.5 sim/s | ETA: 7145s
N2/N1 = 3.229 (314/500)
  Progress: 125600/200000 (62.8%) | Rate: 10.4 sim/s | ETA: 7149s
N2/N1 = 3.289 (315/500)
  Progress: 126000/200000 (63.0%) | Rate: 10.3 sim/s | ETA: 7152s
N2/N1 = 3.350 (316/500)
  Progress: 126400/200000 (63.2%) | Rate: 10.3 sim/s | ETA: 7163s
N2/N1 = 3.412 (317/500)
  Progress: 126800/200000 (63.4%) | Rate: 10.2 sim/s | ETA: 7166s
N2/N1 = 3.476 (318/500)
  Progress: 127200/200000 (63.6%) | Rate: 10.2 sim/s | ETA: 7169s
N2/N1 = 3.541 (319/500)
  Progress: 127600/200000 (63.8%) | Rate: 10.1 sim/s | ETA: 7169s
N2/N1 = 3.607 (320/500)
  Progress: 128000/200000 (64.0%) | Rate: 10.0 sim/s | ETA: 7170s
N2/N1 = 3.674 (321/500)
  Progress: 128400/200000 (64.2%) | Rate: 10.0 sim/s | ETA: 7170s
N2/N1 = 3.742 (322/500)
  Progress: 128800/200000 (64.4%) | Rate: 9.9 sim/s | ETA: 7171ss
N2/N1 = 3.812 (323/500)
  Progress: 129200/200000 (64.6%) | Rate: 9.9 sim/s | ETA: 7170s
N2/N1 = 3.883 (324/500)
  Progress: 129600/200000 (64.8%) | Rate: 9.8 sim/s | ETA: 7170s
N2/N1 = 3.955 (325/500)
  Progress: 130000/200000 (65.0%) | Rate: 9.8 sim/s | ETA: 7169s
N2/N1 = 4.029 (326/500)
  Progress: 130400/200000 (65.2%) | Rate: 9.7 sim/s | ETA: 7168s
N2/N1 = 4.104 (327/500)
  Progress: 130800/200000 (65.4%) | Rate: 9.7 sim/s | ETA: 7167s
N2/N1 = 4.181 (328/500)
  Progress: 131200/200000 (65.6%) | Rate: 9.6 sim/s | ETA: 7166s
N2/N1 = 4.259 (329/500)
  Progress: 131600/200000 (65.8%) | Rate: 9.5 sim/s | ETA: 7163s
N2/N1 = 4.338 (330/500)
  Progress: 132000/200000 (66.0%) | Rate: 9.5 sim/s | ETA: 7161s
N2/N1 = 4.419 (331/500)
  Progress: 132400/200000 (66.2%) | Rate: 9.4 sim/s | ETA: 7157s
N2/N1 = 4.501 (332/500)
  Progress: 132800/200000 (66.4%) | Rate: 9.4 sim/s | ETA: 7156s
N2/N1 = 4.585 (333/500)
  Progress: 133200/200000 (66.6%) | Rate: 9.3 sim/s | ETA: 7152s
N2/N1 = 4.670 (334/500)
  Progress: 133600/200000 (66.8%) | Rate: 9.3 sim/s | ETA: 7148s
N2/N1 = 4.757 (335/500)
  Progress: 134000/200000 (67.0%) | Rate: 9.2 sim/s | ETA: 7144s
N2/N1 = 4.846 (336/500)
  Progress: 134400/200000 (67.2%) | Rate: 9.2 sim/s | ETA: 7139s
N2/N1 = 4.936 (337/500)
  Progress: 134800/200000 (67.4%) | Rate: 9.1 sim/s | ETA: 7133s
N2/N1 = 5.028 (338/500)
  Progress: 135200/200000 (67.6%) | Rate: 9.1 sim/s | ETA: 7128s
N2/N1 = 5.122 (339/500)
  Progress: 135600/200000 (67.8%) | Rate: 9.0 sim/s | ETA: 7123s
N2/N1 = 5.217 (340/500)
  Progress: 136000/200000 (68.0%) | Rate: 9.0 sim/s | ETA: 7119s
N2/N1 = 5.314 (341/500)
  Progress: 136400/200000 (68.2%) | Rate: 8.9 sim/s | ETA: 7115s
N2/N1 = 5.413 (342/500)
  Progress: 136800/200000 (68.4%) | Rate: 8.9 sim/s | ETA: 7112s
N2/N1 = 5.514 (343/500)
  Progress: 137200/200000 (68.6%) | Rate: 8.8 sim/s | ETA: 7109s
N2/N1 = 5.617 (344/500)
  Progress: 137600/200000 (68.8%) | Rate: 8.8 sim/s | ETA: 7107s
N2/N1 = 5.722 (345/500)
  Progress: 138000/200000 (69.0%) | Rate: 8.7 sim/s | ETA: 7119s
N2/N1 = 5.828 (346/500)
  Progress: 138400/200000 (69.2%) | Rate: 8.7 sim/s | ETA: 7116s
N2/N1 = 5.937 (347/500)
  Progress: 138800/200000 (69.4%) | Rate: 8.6 sim/s | ETA: 7114s
N2/N1 = 6.047 (348/500)
  Progress: 139200/200000 (69.6%) | Rate: 8.5 sim/s | ETA: 7111s
N2/N1 = 6.160 (349/500)
  Progress: 139600/200000 (69.8%) | Rate: 8.5 sim/s | ETA: 7109s
N2/N1 = 6.275 (350/500)
  Progress: 140000/200000 (70.0%) | Rate: 8.4 sim/s | ETA: 7108s
N2/N1 = 6.392 (351/500)
  Progress: 140400/200000 (70.2%) | Rate: 8.4 sim/s | ETA: 7106s
N2/N1 = 6.511 (352/500)
  Progress: 140800/200000 (70.4%) | Rate: 8.3 sim/s | ETA: 7104s
N2/N1 = 6.632 (353/500)
  Progress: 141200/200000 (70.6%) | Rate: 8.3 sim/s | ETA: 7103s
N2/N1 = 6.756 (354/500)
  Progress: 141600/200000 (70.8%) | Rate: 8.2 sim/s | ETA: 7101s
N2/N1 = 6.881 (355/500)
  Progress: 142000/200000 (71.0%) | Rate: 8.2 sim/s | ETA: 7100s
N2/N1 = 7.010 (356/500)
  Progress: 142400/200000 (71.2%) | Rate: 8.1 sim/s | ETA: 7099s
N2/N1 = 7.140 (357/500)
  Progress: 142800/200000 (71.4%) | Rate: 8.1 sim/s | ETA: 7098s
N2/N1 = 7.273 (358/500)
  Progress: 143200/200000 (71.6%) | Rate: 8.0 sim/s | ETA: 7097s
N2/N1 = 7.409 (359/500)
  Progress: 143600/200000 (71.8%) | Rate: 7.9 sim/s | ETA: 7097s
N2/N1 = 7.547 (360/500)
  Progress: 144000/200000 (72.0%) | Rate: 7.9 sim/s | ETA: 7096s
N2/N1 = 7.687 (361/500)
  Progress: 144400/200000 (72.2%) | Rate: 7.8 sim/s | ETA: 7095s
N2/N1 = 7.830 (362/500)
  Progress: 144800/200000 (72.4%) | Rate: 7.8 sim/s | ETA: 7093s
N2/N1 = 7.976 (363/500)
  Progress: 145200/200000 (72.6%) | Rate: 7.7 sim/s | ETA: 7092s
N2/N1 = 8.125 (364/500)
  Progress: 145600/200000 (72.8%) | Rate: 7.7 sim/s | ETA: 7091s
N2/N1 = 8.276 (365/500)
  Progress: 146000/200000 (73.0%) | Rate: 7.6 sim/s | ETA: 7089s
N2/N1 = 8.430 (366/500)
  Progress: 146400/200000 (73.2%) | Rate: 7.6 sim/s | ETA: 7087s
N2/N1 = 8.588 (367/500)
  Progress: 146800/200000 (73.4%) | Rate: 7.5 sim/s | ETA: 7085s
N2/N1 = 8.747 (368/500)
  Progress: 147200/200000 (73.6%) | Rate: 7.5 sim/s | ETA: 7083s
N2/N1 = 8.910 (369/500)
  Progress: 147600/200000 (73.8%) | Rate: 7.4 sim/s | ETA: 7080s
N2/N1 = 9.076 (370/500)
  Progress: 148000/200000 (74.0%) | Rate: 7.3 sim/s | ETA: 7078s
N2/N1 = 9.246 (371/500)
  Progress: 148400/200000 (74.2%) | Rate: 7.3 sim/s | ETA: 7074s
N2/N1 = 9.418 (372/500)
  Progress: 148800/200000 (74.4%) | Rate: 7.2 sim/s | ETA: 7072s
N2/N1 = 9.593 (373/500)
  Progress: 149200/200000 (74.6%) | Rate: 7.2 sim/s | ETA: 7069s
N2/N1 = 9.772 (374/500)
  Progress: 149600/200000 (74.8%) | Rate: 7.1 sim/s | ETA: 7065s
N2/N1 = 9.954 (375/500)
  Progress: 150000/200000 (75.0%) | Rate: 7.1 sim/s | ETA: 7062s
N2/N1 = 10.139 (376/500)
  Progress: 150400/200000 (75.2%) | Rate: 7.0 sim/s | ETA: 7058s
N2/N1 = 10.328 (377/500)
  Progress: 150800/200000 (75.4%) | Rate: 7.0 sim/s | ETA: 7053s
N2/N1 = 10.521 (378/500)
  Progress: 151200/200000 (75.6%) | Rate: 6.9 sim/s | ETA: 7049s
N2/N1 = 10.717 (379/500)
  Progress: 151600/200000 (75.8%) | Rate: 6.9 sim/s | ETA: 7044s
N2/N1 = 10.916 (380/500)
  Progress: 152000/200000 (76.0%) | Rate: 6.8 sim/s | ETA: 7039s
N2/N1 = 11.120 (381/500)
  Progress: 152400/200000 (76.2%) | Rate: 6.8 sim/s | ETA: 7034s
N2/N1 = 11.327 (382/500)
  Progress: 152800/200000 (76.4%) | Rate: 6.7 sim/s | ETA: 7028s
N2/N1 = 11.538 (383/500)
  Progress: 153200/200000 (76.6%) | Rate: 6.7 sim/s | ETA: 7022s
N2/N1 = 11.753 (384/500)
  Progress: 153600/200000 (76.8%) | Rate: 6.6 sim/s | ETA: 7016s
N2/N1 = 11.972 (385/500)
  Progress: 154000/200000 (77.0%) | Rate: 6.6 sim/s | ETA: 7009s
N2/N1 = 12.195 (386/500)
  Progress: 154400/200000 (77.2%) | Rate: 6.5 sim/s | ETA: 7002s
N2/N1 = 12.422 (387/500)
  Progress: 154800/200000 (77.4%) | Rate: 6.5 sim/s | ETA: 6995s
N2/N1 = 12.653 (388/500)
  Progress: 155200/200000 (77.6%) | Rate: 6.4 sim/s | ETA: 6987s
N2/N1 = 12.889 (389/500)
  Progress: 155600/200000 (77.8%) | Rate: 6.4 sim/s | ETA: 6979s
N2/N1 = 13.129 (390/500)
  Progress: 156000/200000 (78.0%) | Rate: 6.3 sim/s | ETA: 6971s
N2/N1 = 13.374 (391/500)
  Progress: 156400/200000 (78.2%) | Rate: 6.3 sim/s | ETA: 6964s
N2/N1 = 13.623 (392/500)
  Progress: 156800/200000 (78.4%) | Rate: 6.2 sim/s | ETA: 6954s
N2/N1 = 13.877 (393/500)
  Progress: 157200/200000 (78.6%) | Rate: 6.2 sim/s | ETA: 6944s
N2/N1 = 14.135 (394/500)
  Progress: 157600/200000 (78.8%) | Rate: 6.1 sim/s | ETA: 6934s
N2/N1 = 14.398 (395/500)
  Progress: 158000/200000 (79.0%) | Rate: 6.1 sim/s | ETA: 6923s
N2/N1 = 14.667 (396/500)
  Progress: 158400/200000 (79.2%) | Rate: 6.0 sim/s | ETA: 6911s
N2/N1 = 14.940 (397/500)
  Progress: 158800/200000 (79.4%) | Rate: 6.0 sim/s | ETA: 6899s
N2/N1 = 15.218 (398/500)
  Progress: 159200/200000 (79.6%) | Rate: 5.9 sim/s | ETA: 6885s
N2/N1 = 15.502 (399/500)
  Progress: 159600/200000 (79.8%) | Rate: 5.9 sim/s | ETA: 6872s
N2/N1 = 15.791 (400/500)
  Progress: 160000/200000 (80.0%) | Rate: 5.7 sim/s | ETA: 7020s
N2/N1 = 16.085 (401/500)
  Progress: 160400/200000 (80.2%) | Rate: 5.7 sim/s | ETA: 7004s
N2/N1 = 16.384 (402/500)
  Progress: 160800/200000 (80.4%) | Rate: 5.6 sim/s | ETA: 6987s
N2/N1 = 16.690 (403/500)
  Progress: 161200/200000 (80.6%) | Rate: 5.6 sim/s | ETA: 6969s
N2/N1 = 17.000 (404/500)
  Progress: 161600/200000 (80.8%) | Rate: 5.4 sim/s | ETA: 7166s
N2/N1 = 17.317 (405/500)
  Progress: 162000/200000 (81.0%) | Rate: 5.3 sim/s | ETA: 7145s
N2/N1 = 17.640 (406/500)
  Progress: 162400/200000 (81.2%) | Rate: 5.3 sim/s | ETA: 7125s
N2/N1 = 17.968 (407/500)
  Progress: 162800/200000 (81.4%) | Rate: 5.2 sim/s | ETA: 7103s
N2/N1 = 18.303 (408/500)
  Progress: 163200/200000 (81.6%) | Rate: 5.2 sim/s | ETA: 7080s
N2/N1 = 18.644 (409/500)
  Progress: 163600/200000 (81.8%) | Rate: 5.2 sim/s | ETA: 7057s
N2/N1 = 18.991 (410/500)
  Progress: 164000/200000 (82.0%) | Rate: 5.1 sim/s | ETA: 7034s
N2/N1 = 19.345 (411/500)
  Progress: 164400/200000 (82.2%) | Rate: 5.1 sim/s | ETA: 7010s
N2/N1 = 19.706 (412/500)
  Progress: 164800/200000 (82.4%) | Rate: 5.0 sim/s | ETA: 6985s
N2/N1 = 20.073 (413/500)
  Progress: 165200/200000 (82.6%) | Rate: 5.0 sim/s | ETA: 6957s
N2/N1 = 20.447 (414/500)
  Progress: 165600/200000 (82.8%) | Rate: 5.0 sim/s | ETA: 6930s
N2/N1 = 20.828 (415/500)
  Progress: 166000/200000 (83.0%) | Rate: 4.9 sim/s | ETA: 6901s
N2/N1 = 21.216 (416/500)
  Progress: 166400/200000 (83.2%) | Rate: 4.9 sim/s | ETA: 6872s
N2/N1 = 21.611 (417/500)
  Progress: 166800/200000 (83.4%) | Rate: 4.9 sim/s | ETA: 6843s
N2/N1 = 22.013 (418/500)
  Progress: 167200/200000 (83.6%) | Rate: 4.8 sim/s | ETA: 6813s
N2/N1 = 22.423 (419/500)
  Progress: 167600/200000 (83.8%) | Rate: 4.8 sim/s | ETA: 6798s
N2/N1 = 22.841 (420/500)
  Progress: 168000/200000 (84.0%) | Rate: 4.7 sim/s | ETA: 6766s
N2/N1 = 23.267 (421/500)
  Progress: 168400/200000 (84.2%) | Rate: 4.7 sim/s | ETA: 6734s
N2/N1 = 23.700 (422/500)
  Progress: 168800/200000 (84.4%) | Rate: 4.7 sim/s | ETA: 6700s
N2/N1 = 24.142 (423/500)
  Progress: 169200/200000 (84.6%) | Rate: 4.6 sim/s | ETA: 6664s
N2/N1 = 24.591 (424/500)
  Progress: 169600/200000 (84.8%) | Rate: 4.6 sim/s | ETA: 6628s
N2/N1 = 25.049 (425/500)
  Progress: 170000/200000 (85.0%) | Rate: 4.6 sim/s | ETA: 6592s
N2/N1 = 25.516 (426/500)
  Progress: 170400/200000 (85.2%) | Rate: 4.5 sim/s | ETA: 6553s
N2/N1 = 25.991 (427/500)
  Progress: 170800/200000 (85.4%) | Rate: 4.5 sim/s | ETA: 6514s
N2/N1 = 26.476 (428/500)
  Progress: 171200/200000 (85.6%) | Rate: 4.4 sim/s | ETA: 6476s
N2/N1 = 26.969 (429/500)
  Progress: 171600/200000 (85.8%) | Rate: 4.4 sim/s | ETA: 6436s
N2/N1 = 27.471 (430/500)
  Progress: 172000/200000 (86.0%) | Rate: 4.4 sim/s | ETA: 6395s
N2/N1 = 27.983 (431/500)
  Progress: 172400/200000 (86.2%) | Rate: 4.3 sim/s | ETA: 6352s
N2/N1 = 28.504 (432/500)
  Progress: 172800/200000 (86.4%) | Rate: 4.3 sim/s | ETA: 6309s
N2/N1 = 29.035 (433/500)
  Progress: 173200/200000 (86.6%) | Rate: 4.3 sim/s | ETA: 6265s
N2/N1 = 29.576 (434/500)
  Progress: 173600/200000 (86.8%) | Rate: 4.2 sim/s | ETA: 6219s
N2/N1 = 30.127 (435/500)
  Progress: 174000/200000 (87.0%) | Rate: 4.2 sim/s | ETA: 6172s
N2/N1 = 30.688 (436/500)
  Progress: 174400/200000 (87.2%) | Rate: 4.2 sim/s | ETA: 6124s
N2/N1 = 31.260 (437/500)
  Progress: 174800/200000 (87.4%) | Rate: 4.1 sim/s | ETA: 6075s
N2/N1 = 31.842 (438/500)
  Progress: 175200/200000 (87.6%) | Rate: 4.1 sim/s | ETA: 6093s
N2/N1 = 32.436 (439/500)
  Progress: 175600/200000 (87.8%) | Rate: 4.0 sim/s | ETA: 6042s
N2/N1 = 33.040 (440/500)
  Progress: 176000/200000 (88.0%) | Rate: 4.0 sim/s | ETA: 5989s
N2/N1 = 33.655 (441/500)
  Progress: 176400/200000 (88.2%) | Rate: 4.0 sim/s | ETA: 5940s
N2/N1 = 34.282 (442/500)
  Progress: 176800/200000 (88.4%) | Rate: 3.9 sim/s | ETA: 5884s
N2/N1 = 34.921 (443/500)
  Progress: 177200/200000 (88.6%) | Rate: 3.9 sim/s | ETA: 5827s
N2/N1 = 35.572 (444/500)
  Progress: 177600/200000 (88.8%) | Rate: 3.9 sim/s | ETA: 5768s
N2/N1 = 36.234 (445/500)
  Progress: 178000/200000 (89.0%) | Rate: 3.9 sim/s | ETA: 5710s
N2/N1 = 36.909 (446/500)
  Progress: 178400/200000 (89.2%) | Rate: 3.8 sim/s | ETA: 5650s
N2/N1 = 37.597 (447/500)
  Progress: 178800/200000 (89.4%) | Rate: 3.8 sim/s | ETA: 5589s
N2/N1 = 38.297 (448/500)
  Progress: 179200/200000 (89.6%) | Rate: 3.6 sim/s | ETA: 5831s
N2/N1 = 39.011 (449/500)
  Progress: 179600/200000 (89.8%) | Rate: 3.3 sim/s | ETA: 6146s
N2/N1 = 39.737 (450/500)
  Progress: 180000/200000 (90.0%) | Rate: 3.3 sim/s | ETA: 6065s
N2/N1 = 40.478 (451/500)
  Progress: 180400/200000 (90.2%) | Rate: 3.3 sim/s | ETA: 5983s
N2/N1 = 41.232 (452/500)
  Progress: 180800/200000 (90.4%) | Rate: 3.3 sim/s | ETA: 5899s
N2/N1 = 42.000 (453/500)
  Progress: 181200/200000 (90.6%) | Rate: 3.2 sim/s | ETA: 5814s
N2/N1 = 42.782 (454/500)
  Progress: 181600/200000 (90.8%) | Rate: 3.2 sim/s | ETA: 5727s
N2/N1 = 43.579 (455/500)
  Progress: 182000/200000 (91.0%) | Rate: 3.2 sim/s | ETA: 5638s
N2/N1 = 44.391 (456/500)
  Progress: 182400/200000 (91.2%) | Rate: 3.2 sim/s | ETA: 5548s
N2/N1 = 45.218 (457/500)
  Progress: 182800/200000 (91.4%) | Rate: 3.1 sim/s | ETA: 5549s
N2/N1 = 46.060 (458/500)
  Progress: 183200/200000 (91.6%) | Rate: 3.1 sim/s | ETA: 5454s
N2/N1 = 46.918 (459/500)
  Progress: 183600/200000 (91.8%) | Rate: 3.1 sim/s | ETA: 5359s
N2/N1 = 47.792 (460/500)
  Progress: 184000/200000 (92.0%) | Rate: 3.0 sim/s | ETA: 5261s
N2/N1 = 48.683 (461/500)
  Progress: 184400/200000 (92.2%) | Rate: 3.0 sim/s | ETA: 5163s
N2/N1 = 49.590 (462/500)
  Progress: 184800/200000 (92.4%) | Rate: 3.0 sim/s | ETA: 5063s
N2/N1 = 50.513 (463/500)
  Progress: 185200/200000 (92.6%) | Rate: 3.0 sim/s | ETA: 4961s
N2/N1 = 51.454 (464/500)
  Progress: 185600/200000 (92.8%) | Rate: 3.0 sim/s | ETA: 4858s
N2/N1 = 52.413 (465/500)
  Progress: 186000/200000 (93.0%) | Rate: 2.9 sim/s | ETA: 4753s
N2/N1 = 53.389 (466/500)
  Progress: 186400/200000 (93.2%) | Rate: 2.9 sim/s | ETA: 4647s
N2/N1 = 54.384 (467/500)
  Progress: 186800/200000 (93.4%) | Rate: 2.9 sim/s | ETA: 4540s
N2/N1 = 55.397 (468/500)
  Progress: 187200/200000 (93.6%) | Rate: 2.9 sim/s | ETA: 4430s
N2/N1 = 56.429 (469/500)
  Progress: 187600/200000 (93.8%) | Rate: 2.9 sim/s | ETA: 4320s
N2/N1 = 57.480 (470/500)
  Progress: 188000/200000 (94.0%) | Rate: 2.9 sim/s | ETA: 4208s
N2/N1 = 58.551 (471/500)
  Progress: 188400/200000 (94.2%) | Rate: 2.8 sim/s | ETA: 4095s
N2/N1 = 59.642 (472/500)
  Progress: 188800/200000 (94.4%) | Rate: 2.8 sim/s | ETA: 3980s
N2/N1 = 60.753 (473/500)
  Progress: 189200/200000 (94.6%) | Rate: 2.8 sim/s | ETA: 3863s
N2/N1 = 61.885 (474/500)
  Progress: 189600/200000 (94.8%) | Rate: 2.8 sim/s | ETA: 3745s
N2/N1 = 63.038 (475/500)
  Progress: 190000/200000 (95.0%) | Rate: 2.8 sim/s | ETA: 3625s
N2/N1 = 64.212 (476/500)
  Progress: 190400/200000 (95.2%) | Rate: 2.7 sim/s | ETA: 3504s
N2/N1 = 65.408 (477/500)
  Progress: 190800/200000 (95.4%) | Rate: 2.7 sim/s | ETA: 3380s
N2/N1 = 66.627 (478/500)
  Progress: 191200/200000 (95.6%) | Rate: 2.7 sim/s | ETA: 3255s
N2/N1 = 67.868 (479/500)
  Progress: 191600/200000 (95.8%) | Rate: 2.7 sim/s | ETA: 3128s
N2/N1 = 69.132 (480/500)
  Progress: 192000/200000 (96.0%) | Rate: 2.7 sim/s | ETA: 3001s
N2/N1 = 70.420 (481/500)
  Progress: 192400/200000 (96.2%) | Rate: 2.6 sim/s | ETA: 2871s
N2/N1 = 71.732 (482/500)
  Progress: 192800/200000 (96.4%) | Rate: 2.6 sim/s | ETA: 2739s
N2/N1 = 73.068 (483/500)
  Progress: 193200/200000 (96.6%) | Rate: 2.6 sim/s | ETA: 2605s
N2/N1 = 74.429 (484/500)
  Progress: 193600/200000 (96.8%) | Rate: 2.6 sim/s | ETA: 2469s
N2/N1 = 75.816 (485/500)
  Progress: 194000/200000 (97.0%) | Rate: 2.6 sim/s | ETA: 2331s
N2/N1 = 77.228 (486/500)
  Progress: 194400/200000 (97.2%) | Rate: 2.6 sim/s | ETA: 2191s
N2/N1 = 78.667 (487/500)
  Progress: 194800/200000 (97.4%) | Rate: 2.5 sim/s | ETA: 2049s
N2/N1 = 80.132 (488/500)
  Progress: 195200/200000 (97.6%) | Rate: 2.5 sim/s | ETA: 1905s
N2/N1 = 81.625 (489/500)
  Progress: 195600/200000 (97.8%) | Rate: 2.5 sim/s | ETA: 1758s
N2/N1 = 83.146 (490/500)
  Progress: 196000/200000 (98.0%) | Rate: 2.5 sim/s | ETA: 1609s
N2/N1 = 84.695 (491/500)
  Progress: 196400/200000 (98.2%) | Rate: 2.5 sim/s | ETA: 1459s
N2/N1 = 86.272 (492/500)
  Progress: 196800/200000 (98.4%) | Rate: 2.5 sim/s | ETA: 1306s
N2/N1 = 87.880 (493/500)
  Progress: 197200/200000 (98.6%) | Rate: 2.4 sim/s | ETA: 1151s
N2/N1 = 89.517 (494/500)
  Progress: 197600/200000 (98.8%) | Rate: 2.4 sim/s | ETA: 993ss
N2/N1 = 91.184 (495/500)
  Progress: 198000/200000 (99.0%) | Rate: 2.4 sim/s | ETA: 836s
N2/N1 = 92.883 (496/500)
  Progress: 198400/200000 (99.2%) | Rate: 2.4 sim/s | ETA: 673s
N2/N1 = 94.613 (497/500)
  Progress: 198800/200000 (99.4%) | Rate: 2.4 sim/s | ETA: 509s
N2/N1 = 96.376 (498/500)
  Progress: 199200/200000 (99.6%) | Rate: 2.3 sim/s | ETA: 341s
N2/N1 = 98.171 (499/500)
  Progress: 199600/200000 (99.8%) | Rate: 2.3 sim/s | ETA: 172s
N2/N1 = 100.000 (500/500)
  Progress: 200000/200000 (100.0%) | Rate: 2.3 sim/s | ETA: 0ss

Scan completed in 86785.2s (1446.4 min)
Average: 0.43s per simulation

Results Summary:
  Valid orbits:         161465 (80.7%)
  Collisions:           38535 (19.3%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.000000
  Max:     1.489600
  Mean:    0.290818
  Median:  0.257164
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 22. Image of quantitative heat map. Similar patterns are seen as compared to the discrete phase space diagram. Notably, there is a stark contrast in relative standard deviation between the lower third region and the neon band above it. Collisions increase as charge ratio increases.

Code Block Summary. The above code adapts the previous code for a discrete quantitative phase space scan to a continuous one, with relative standard deviation of an electorn's orbital period as a function of initial velocity angle and charge ratio.

Streaks Neon Quasi

Analysis for Continuous Quantitative Phase Space Scan¶

The above phase space scan was run for 200000 simulations, and is hence much more detailed and informative. We see that the continuous heat map outlines similar patterns compared to the discrete phase-space diagram. However, there are several additional observations we can make using relative standard deviation:

  1. Collision Patterns: Unlike the discrete phase space, we see that the collision orbits (in red) follow interesting patterns as charge ratio increases. For instance, we see three curves of collisions on the left side of the graph. Furthermore, along the top of the graph, the collisions trace out a distinct pattern that look somewhat like the feathers of a bird. Again, this is something that the previous phase span did not show.
  2. Neon band above lower third region: We see that there is a neon bandright above the lower third region of stable orbits (purple), indicating a large number of unstable orbits along a curve. The transition between stable to unstable orbits (purple to green to yellow) is surprisingly very sudden, with no indication of a gradual change in relative standard deviation
  3. Curves of periodicity & quasi-periodicity: When charge ratio is between 1 and 10, we see curves of quasi-periodicity, characterized by distinct colour (such as neon, turquoise, and purple). The curves of quasi-periodicity are especially interesting behaviour because it could never be detected just by a discrete phase space analysis. These curves connect down to the lower regions of the graph and are seen as streaks the follow a counterclockwise path top down. We also see a curve of periodicity in the center of the graph, seens as a purple streak.

For this portion of analysis, we look at these patterns in more detail.

We first create two new functions that allow for a partial phase space scan of our domain: This will allow us to "zoom into" specific regions we wish to study.

In [71]:
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math 
import time

# Fixed Constants
k = 8.99e9            # Coulomb constant, N·m²/C²
e = -1.6e-19          # Electron charge magnitude, C
n1 = 1.6e-19          # Nucleus 1 charge 
me = 9.109e-31        # Electron mass, kg
r0 = 5.29e-11         # Bohr radius, m 

# Orbital time
T = 2.5e-15  # time for simulation to run, s

# Nuclei coordinates
distance = 1.2e-10  # vertical distance between two nuclei, m
y1 = distance / 2   # N1 y coordinate, m 
x1 = 0              # N1 x coordinate, m
y2 = -distance / 2  # N2 x coordinate, m
x2 = 0              # N2 y coordinate, m

# Initial Velocity
v0 = 2.18e6  # initial velocity (m/s)

# Define your diff_eqns function
def diff_eqns(t, state):
    x, y, vx, vy = state
    
    r1x = x - x1
    r1y = y - y1
    r1 = np.sqrt(r1x**2 + r1y**2)
    r2x = x - x2
    r2y = y - y2
    r2 = np.sqrt(r2x**2 + r2y**2)
    
    # Avoid singularities
    r1 = max(r1, 1e-20)
    r2 = max(r2, 1e-20)
    
    # Calculate force & acceleration components for N1
    fx1 = k * e * n1 * r1x / r1**3
    fy1 = k * e * n1 * r1y / r1**3
    # Calculate force & acceleration components for N2
    fx2 = k * e * n2 * r2x / r2**3
    fy2 = k * e * n2 * r2y / r2**3 
    fx = fx1 + fx2 
    fy = fy1 + fy2
    # Calculate acceleration
    accx = fx / me 
    accy = fy / me  
    # Return differentials
    return vx, vy, accx, accy

# REVISED DETECTION FUNCTIONS

def check_collision(sol, collision_threshold=8.5e-16):
    r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
    r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
    
    collision_n1_indices = np.where(r1 < collision_threshold)[0]
    collision_n2_indices = np.where(r2 < collision_threshold)[0]
    
    if len(collision_n1_indices) > 0 or len(collision_n2_indices) > 0:
        return True
    return False

def calculate_period_std(sol):
    # Calculate distance from starting position
    x0, y0 = sol.y[0][0], sol.y[1][0]
    distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
    
    # Find local minima (returns to starting region)
    try:
        peaks, _ = find_peaks(-distances, distance=len(distances)//20)
    except:
        return None
    
    if len(peaks) < 2:
        return None
    
    # Calculate periods
    periods = np.diff(sol.t[peaks])
    
    if len(periods) < 2:
        return None
    
    # Calculate relative standard deviation
    period_mean = np.mean(periods)
    period_std = np.std(periods)
    
    if period_mean == 0:
        return None
    
    period_std_rel = period_std / period_mean
    
    return period_std_rel

def analyze_single_orbit_quantitative(angle, n2_value):
    try:
        # Set up initial conditions
        theta = math.radians(angle)
        vx0 = math.cos(theta) * v0
        vy0 = math.sin(theta) * v0
        state0 = (r0, 0, vx0, vy0)
        
        # Set n2 as global for diff_eqns to use
        global n2
        n2 = n2_value
        
        # Run simulation
        t_span = (0, T)
        sol = solve_ivp(diff_eqns, t_span, state0, 
                       rtol=1e-9, atol=1e-9, 
                       max_step=1e-17)
        
        # Check for collision first
        collided = check_collision(sol)
        if collided:
            return -1  # Collision marker
        
        # Calculate period variability
        rel_std = calculate_period_std(sol)
        return rel_std
            
    except Exception as e:
        return None  # Error

def scan_phase_space_partial(n_theta, n_n2, theta_min, theta_max, n_min, n_max):
    print("="*70)
    print("PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability")
    print("="*70)
    
    # Phase space ranges
    n1 = 1.6e-19
    theta_range = np.linspace(theta_min, theta_max, n_theta)
    n2_n1_ratios = np.linspace(n_min, n_max, n_n2)
    n2_range = n1 * n2_n1_ratios
    
    print(f"Theta axis: {n_theta} points from 0° to 90°")
    print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
    print(f"  N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
    print(f"Total simulations: {n_theta * n_n2}")
    print("="*70 + "\n")
    
    # Initialize results grid (use NaN for missing data)
    results_grid = np.full((n_n2, n_theta), np.nan)
    
    total_sims = n_theta * n_n2
    sim_count = 0
    start_time = time.time()
    
    # Run simulations
    for i, n2_val in enumerate(n2_range):
        print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
        for j, angle_val in enumerate(theta_range):
            sim_count += 1
            
            if sim_count % 15 == 0 or sim_count == total_sims:
                elapsed = time.time() - start_time
                rate = sim_count / elapsed if elapsed > 0 else 0
                eta = (total_sims - sim_count) / rate if rate > 0 else 0
                print(f"  Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
                      f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
            
            # Analyze this configuration
            rel_std = analyze_single_orbit_quantitative(angle_val, n2_val)
            results_grid[i, j] = rel_std if rel_std is not None else np.nan
    
    elapsed = time.time() - start_time
    print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
    print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
    
    # Print statistics
    n_collision = np.sum(results_grid == -1)
    n_valid = np.sum(~np.isnan(results_grid) & (results_grid != -1))
    n_error = np.sum(np.isnan(results_grid))
    
    valid_data = results_grid[(~np.isnan(results_grid)) & (results_grid != -1)]
    
    print("Results Summary:")
    print(f"  Valid orbits:         {n_valid:4d} ({100*n_valid/total_sims:.1f}%)")
    print(f"  Collisions:           {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
    print(f"  Errors:               {n_error:4d} ({100*n_error/total_sims:.1f}%)")
    
    if len(valid_data) > 0:
        print(f"\nPeriod Rel. Std. Dev. Statistics (valid orbits):")
        print(f"  Min:     {np.min(valid_data):.6f}")
        print(f"  Max:     {np.max(valid_data):.6f}")
        print(f"  Mean:    {np.mean(valid_data):.6f}")
        print(f"  Median:  {np.median(valid_data):.6f}")
    
    print("="*70 + "\n")
    
    return results_grid, theta_range, n2_range

# PLOTTING RESULTS

def plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max,n_min, n_max):
    import matplotlib.colors as mcolors
    
    fig = plt.figure(figsize=(10, 8))
    
    # Create meshgrid
    theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
    n2_n1_grid = n2_grid / n1
    
    # Separate collision and non-collision data
    collision_mask = (results_grid == -1)
    data_for_heatmap = results_grid.copy()
    data_for_heatmap[collision_mask] = np.nan  # Hide collisions from main heatmap
    
    ax = fig.add_subplot(111)
    
    # Plot main heat map (period variability)
    im = ax.pcolormesh(theta_grid, n2_n1_grid, data_for_heatmap,
                       cmap='viridis', shading='auto',
                       vmin=0, vmax=np.nanpercentile(data_for_heatmap, 95))
    
    # Overlay collision regions in red
    collision_data = np.where(collision_mask, 1, np.nan)
    ax.pcolormesh(theta_grid, n2_n1_grid, collision_data,
                  cmap=mcolors.ListedColormap(['#ff0000']), 
                  shading='auto', alpha=0.9)
    
    ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
    ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
    ax.set_title('Phase Space: Orbital Period Variability (Rel. Std. Dev.)\n' +
                 'θ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1], V0 = 2.18e6 m/s', 
                 fontsize=16, fontweight='bold', pad=20)
    
    # Use log scale for N2/N1
    ax.set_ylim([n_min, n_max])
    #ax.set_yticks([0.01, 0.1, 1, 10, 100])
   #ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
    ax.set_xlim([theta_min, theta_max])
    
    # Add grid
    ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
    
    # Add reference lines
    ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5, 
               alpha=0.8, label='N2 = N1')
    ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
    
    # Add colorbar for period variability
    cbar = plt.colorbar(im, ax=ax, pad=0.02, aspect=30)
    cbar.set_label('Relative Std. Dev. of Period\n(Lower = More Periodic)', 
                   fontsize=12, fontweight='bold')
    
    # Add legend for collision regions
    from matplotlib.patches import Patch
    legend_elements = [
        Patch(facecolor='#ff0000', label='Collision'),
        Patch(facecolor='#440154', label='Low Variability (Stable)'),
        Patch(facecolor='#fde724', label='High Variability (Chaotic)')
    ]
    ax.legend(handles=legend_elements, loc='upper left', fontsize=10,
             framealpha=0.9)
    
    plt.tight_layout()
    plt.savefig('phase_space_heatmap_quantitative.png', dpi=300, bbox_inches='tight')
    print("Heat map saved as 'phase_space_heatmap_quantitative.png'")
    plt.show()
    
    return fig

Code Block Summary. These above code adapt the work from the discrete phase space scan to create two new functions, scan_phase_space_partia and plot_phase_space_partial. INstead of taking the $\theta$ and $\frac{N_2}{N_1}$ range as fixed domains, we pass them as parameters in our function, so that we can "Zoom in" and "Zoom out" of regions we want to analyze.

Pattern 1: Periodic Collisions¶

Periodic

We first analyze the periodic collision patterns observed when the charge ratio is very large. To do this, we first conduct a partial phase space simulation to zoom into the region of study.

In [44]:
import numpy as np 

print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

theta_min = 75
theta_max = 90 
n_min = 80
n_max = 100

results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=30, n_n2=30, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)

# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)

# Save results
np.savez('partial_phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 30 points from 0° to 90°
N2 axis: 30 points from N1/100 to 100*N1
  N2/N1 range: 80.000 to 100.0
Total simulations: 900
======================================================================


N2/N1 = 80.000 (1/30)
  Progress: 30/900 (3.3%) | Rate: 0.4 sim/s | ETA: 2485s
N2/N1 = 80.690 (2/30)
  Progress: 60/900 (6.7%) | Rate: 0.3 sim/s | ETA: 2469s
N2/N1 = 81.379 (3/30)
  Progress: 90/900 (10.0%) | Rate: 0.3 sim/s | ETA: 2401s
N2/N1 = 82.069 (4/30)
  Progress: 120/900 (13.3%) | Rate: 0.3 sim/s | ETA: 2319s
N2/N1 = 82.759 (5/30)
  Progress: 150/900 (16.7%) | Rate: 0.3 sim/s | ETA: 2257s
N2/N1 = 83.448 (6/30)
  Progress: 180/900 (20.0%) | Rate: 0.3 sim/s | ETA: 2180s
N2/N1 = 84.138 (7/30)
  Progress: 210/900 (23.3%) | Rate: 0.2 sim/s | ETA: 4415s
N2/N1 = 84.828 (8/30)
  Progress: 240/900 (26.7%) | Rate: 0.0 sim/s | ETA: 29370s
N2/N1 = 85.517 (9/30)
  Progress: 270/900 (30.0%) | Rate: 0.0 sim/s | ETA: 25137s
N2/N1 = 86.207 (10/30)
  Progress: 300/900 (33.3%) | Rate: 0.0 sim/s | ETA: 21737s
N2/N1 = 86.897 (11/30)
  Progress: 330/900 (36.7%) | Rate: 0.0 sim/s | ETA: 18936s
N2/N1 = 87.586 (12/30)
  Progress: 360/900 (40.0%) | Rate: 0.0 sim/s | ETA: 16589s
N2/N1 = 88.276 (13/30)
  Progress: 390/900 (43.3%) | Rate: 0.0 sim/s | ETA: 14589s
N2/N1 = 88.966 (14/30)
  Progress: 420/900 (46.7%) | Rate: 0.0 sim/s | ETA: 12860s
N2/N1 = 89.655 (15/30)
  Progress: 450/900 (50.0%) | Rate: 0.0 sim/s | ETA: 11350s
N2/N1 = 90.345 (16/30)
  Progress: 480/900 (53.3%) | Rate: 0.0 sim/s | ETA: 10019s
N2/N1 = 91.034 (17/30)
  Progress: 510/900 (56.7%) | Rate: 0.0 sim/s | ETA: 8833s
N2/N1 = 91.724 (18/30)
  Progress: 540/900 (60.0%) | Rate: 0.0 sim/s | ETA: 7766s
N2/N1 = 92.414 (19/30)
  Progress: 570/900 (63.3%) | Rate: 0.0 sim/s | ETA: 6800s
N2/N1 = 93.103 (20/30)
  Progress: 600/900 (66.7%) | Rate: 0.1 sim/s | ETA: 5922s
N2/N1 = 93.793 (21/30)
  Progress: 630/900 (70.0%) | Rate: 0.1 sim/s | ETA: 5117s
N2/N1 = 94.483 (22/30)
  Progress: 660/900 (73.3%) | Rate: 0.1 sim/s | ETA: 4378s
N2/N1 = 95.172 (23/30)
  Progress: 690/900 (76.7%) | Rate: 0.1 sim/s | ETA: 3694s
N2/N1 = 95.862 (24/30)
  Progress: 720/900 (80.0%) | Rate: 0.1 sim/s | ETA: 3059s
N2/N1 = 96.552 (25/30)
  Progress: 750/900 (83.3%) | Rate: 0.1 sim/s | ETA: 2467s
N2/N1 = 97.241 (26/30)
  Progress: 780/900 (86.7%) | Rate: 0.1 sim/s | ETA: 1913s
N2/N1 = 97.931 (27/30)
  Progress: 810/900 (90.0%) | Rate: 0.1 sim/s | ETA: 1393s
N2/N1 = 98.621 (28/30)
  Progress: 840/900 (93.3%) | Rate: 0.1 sim/s | ETA: 902ss
N2/N1 = 99.310 (29/30)
  Progress: 870/900 (96.7%) | Rate: 0.1 sim/s | ETA: 439s
N2/N1 = 100.000 (30/30)
  Progress: 900/900 (100.0%) | Rate: 0.1 sim/s | ETA: 0ss

Scan completed in 12838.0s (214.0 min)
Average: 14.26s per simulation

Results Summary:
  Valid orbits:          194 (21.6%)
  Collisions:            706 (78.4%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.197193
  Max:     0.277826
  Mean:    0.232681
  Median:  0.230353
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 23. Zoom-in phase space simulation of collision streaks, with domain $75 \leq \theta \leq 90$ and $80 \leq \frac{N_2}{N_1} \leq 100$. Note that the colours in this phase space are different from the original phase scan, since the range in the colour bar for relative standard deviation of period is lower. The $\frac{N_2}{N_1}$ range is displayed as a linear scale.

The periodic curves of collision orbits seen in the original phase space graph are even more pronounced in Figure 23 with the red indicating collision orbits, and green indicating orbits with relative standard deviations of approximately 0.2-0.25. This allows us to pick one combination of parameters that is a collision, and one that is not to sstudy the differences in behaviour more carefully.

Pattern 1: Non-collision Orbit ($N_2 = 82.5 N_1, \theta = 80$)¶

We first start by looking at the orbital trajectory of a non-collision orbit, where we pick $N_2 = 82.5 N_1, \theta = 80$.

In [142]:
# Parameters to vary
n2 = 80 * 1.6e-19  # Nucleus 2 charge
angle = 83  # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.1, T, 80)
# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.276e-19 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.292e-11 ... -6.759e-12 -6.724e-12]
            [ 0.000e+00  2.571e-13 ... -6.898e-11 -6.900e-11]
            [ 2.657e+05 -3.321e+03 ...  4.495e+07  4.503e+07]
            [ 2.164e+06  1.866e+06 ... -3.276e+07 -3.266e+07]]
      sol: None
 t_events: None
 y_events: None
     nfev: 286964
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 24. When $N_2 = 1.3e17 C$ and $\theta = 80$, the electron exhibits a trajectory that does not collide with the nucleus.

From the above, we see that the electorn orbits $N_2$ in an ellptical pattern, but exhibits enough drift to vary its periodic position over time, forming a half-ellptical shape. However, this does not tell us much about the collision aspect of the orbit. To understand this deeper, we generate graphs plotting the distance between the electron, $N_1$ and $N_2$; since the electron is much closer $N_2$, we also include a third subplot that zooms into the the graph when the electron nearly meets $N_2$.

In [143]:
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2) 


fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()

ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True) 
ax[0].legend(loc = "upper left")

ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1) 
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2") 
ax.scatter(sol.t[index], minimum, c = "crimson",  s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14) 
ax.grid(True) 
ax.legend(loc = "upper left")
Out[143]:
<matplotlib.legend.Legend at 0x1741ba0a0>
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Figure 25a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). We see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape.

From the above, we see that both electron distances from $N_1$ and $N_2$ exhibit oscillatory patterns, where the distances various between local minima and maxima; simultaneously, these oscillations are "packaged" in a sinuosoidal-like pattern, with the amplitudes of the oscillatory patterns growing and shrinking periodically as well. This matches the trajectory plot from Figure 24; while the electron orbits in ellptical atterns around $N_2$, its trajectory also grows and shrinks due drift.

Looking at Figure 25c, we knotice that the minimum distance between $N_2$ and the electron is 1.29e-15m. Hence, this is not counted as a collision orbit.

Pattern 1: Collision Orbit ($N_2 = 85N_1, \theta = 80$)¶

Next, we look at a collision orbit in this same region: we pick $N_2 = 85N_1, \theta = 80$.

In [147]:
# Parameters to vary
n2 = 85 * 1.6e-19  # Nucleus 2 charge
angle = 80  # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.1, T, 80)
# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.317e-19 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.293e-11 ...  7.370e-12  7.716e-12]
            [ 0.000e+00  2.612e-13 ... -6.796e-11 -6.792e-11]
            [ 3.786e+05  8.387e+04 ...  5.815e+07  5.741e+07]
            [ 2.147e+06  1.820e+06 ...  6.556e+06  7.333e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 298478
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? Yes

Figure 26. When $N_2 = 85N_1, \theta = 80$, the electron exhibits a very similar trajectory as the non-collision orbit. However, the trajectory slightly varies so that the electron collides with $N_2$.

From above, we see that the path the electron takes is very similar to that of the non-collision orbit, except that the electron does collide with $N_2$. Again, we look at the distances between the eelectorn, $N_1$, adn $N_2$ to quantify this.

In [ ]:
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2) 


fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()

ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True) 
ax[0].legend(loc = "upper left")

ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1) 
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2") 
ax.scatter(sol.t[index], minimum, c = "crimson",  s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14) 
ax.grid(True) 
ax.legend(loc = "upper left")
Out[ ]:
<matplotlib.legend.Legend at 0x17444f7f0>
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Figure 27 a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). We see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape, just like the non-collision orbit. However, the amplitudes of the oscillations are larger.

We see from above that the graphs of Distance from $N_1$ vs. Time and Distance from $N_2$ vs. Time are very similar to that of the non-collision orbit. Howeve,r looking at Figure 27c, we observe that the amplitudes of the oscillations for $N_2$ is larger; the minimum distnace from $N_2$ is 1.65e-16m, yielding a collision with the nucleus.

Hence, from this comparison, we can infer that the periodic patterns seen in the upper half of the full phase space graph is due to the slight variances in amplitudes in the distances between $N_2$ and the electron, but not due to complete differences in orbital trajectory shapes.

Pattern 2: Claw-Mark Collisions¶

Claw

Next, we look at the upper left portion of the graph, where collisions in a claw-mark like pattern are seen. Again, we first zoom into this region to see the patterns in more detail.

In [9]:
import numpy as np 

print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

theta_min = 0
theta_max = 15
n_min = 5
n_max = 15

results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=30, n_n2=30, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)

# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)

# Save results
np.savez('partial_phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 30 points from 0° to 90°
N2 axis: 30 points from N1/100 to 100*N1
  N2/N1 range: 5.000 to 15.0
Total simulations: 900
======================================================================


N2/N1 = 5.000 (1/30)
  Progress: 30/900 (3.3%) | Rate: 1.7 sim/s | ETA: 501s
N2/N1 = 5.345 (2/30)
  Progress: 60/900 (6.7%) | Rate: 1.7 sim/s | ETA: 496s
N2/N1 = 5.690 (3/30)
  Progress: 90/900 (10.0%) | Rate: 1.6 sim/s | ETA: 494s
N2/N1 = 6.034 (4/30)
  Progress: 120/900 (13.3%) | Rate: 1.6 sim/s | ETA: 486s
N2/N1 = 6.379 (5/30)
  Progress: 150/900 (16.7%) | Rate: 1.6 sim/s | ETA: 477s
N2/N1 = 6.724 (6/30)
  Progress: 180/900 (20.0%) | Rate: 1.5 sim/s | ETA: 469s
N2/N1 = 7.069 (7/30)
  Progress: 210/900 (23.3%) | Rate: 1.5 sim/s | ETA: 460s
N2/N1 = 7.414 (8/30)
  Progress: 240/900 (26.7%) | Rate: 1.5 sim/s | ETA: 448s
N2/N1 = 7.759 (9/30)
  Progress: 270/900 (30.0%) | Rate: 1.4 sim/s | ETA: 436s
N2/N1 = 8.103 (10/30)
  Progress: 300/900 (33.3%) | Rate: 1.4 sim/s | ETA: 424s
N2/N1 = 8.448 (11/30)
  Progress: 330/900 (36.7%) | Rate: 1.4 sim/s | ETA: 409s
N2/N1 = 8.793 (12/30)
  Progress: 360/900 (40.0%) | Rate: 1.4 sim/s | ETA: 394s
N2/N1 = 9.138 (13/30)
  Progress: 390/900 (43.3%) | Rate: 1.3 sim/s | ETA: 378s
N2/N1 = 9.483 (14/30)
  Progress: 420/900 (46.7%) | Rate: 1.3 sim/s | ETA: 361s
N2/N1 = 9.828 (15/30)
  Progress: 450/900 (50.0%) | Rate: 1.3 sim/s | ETA: 344s
N2/N1 = 10.172 (16/30)
  Progress: 480/900 (53.3%) | Rate: 1.3 sim/s | ETA: 326s
N2/N1 = 10.517 (17/30)
  Progress: 510/900 (56.7%) | Rate: 1.3 sim/s | ETA: 307s
N2/N1 = 10.862 (18/30)
  Progress: 540/900 (60.0%) | Rate: 1.3 sim/s | ETA: 288s
N2/N1 = 11.207 (19/30)
  Progress: 570/900 (63.3%) | Rate: 1.2 sim/s | ETA: 267s
N2/N1 = 11.552 (20/30)
  Progress: 600/900 (66.7%) | Rate: 1.2 sim/s | ETA: 246s
N2/N1 = 11.897 (21/30)
  Progress: 630/900 (70.0%) | Rate: 1.2 sim/s | ETA: 225s
N2/N1 = 12.241 (22/30)
  Progress: 660/900 (73.3%) | Rate: 1.2 sim/s | ETA: 202s
N2/N1 = 12.586 (23/30)
  Progress: 690/900 (76.7%) | Rate: 1.2 sim/s | ETA: 179s
N2/N1 = 12.931 (24/30)
  Progress: 720/900 (80.0%) | Rate: 1.2 sim/s | ETA: 156s
N2/N1 = 13.276 (25/30)
  Progress: 750/900 (83.3%) | Rate: 1.1 sim/s | ETA: 131s
N2/N1 = 13.621 (26/30)
  Progress: 780/900 (86.7%) | Rate: 1.1 sim/s | ETA: 106s
N2/N1 = 13.966 (27/30)
  Progress: 810/900 (90.0%) | Rate: 1.1 sim/s | ETA: 81ss
N2/N1 = 14.310 (28/30)
  Progress: 840/900 (93.3%) | Rate: 1.1 sim/s | ETA: 54s
N2/N1 = 14.655 (29/30)
  Progress: 870/900 (96.7%) | Rate: 1.1 sim/s | ETA: 27s
N2/N1 = 15.000 (30/30)
  Progress: 900/900 (100.0%) | Rate: 1.1 sim/s | ETA: 0s

Scan completed in 831.3s (13.9 min)
Average: 0.92s per simulation

Results Summary:
  Valid orbits:          646 (71.8%)
  Collisions:            254 (28.2%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.075750
  Max:     0.368979
  Mean:    0.195883
  Median:  0.193745
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 28. Zoom-in phase space simulation of the region $0 \leq \theta \leq 15$ and $5 \leq \frac{N_2}{N_1} \leq 15$. Streaks of collisions are shown, forming "claw-like" patterns in the phase space graph–this is the "claw mark" we are analyzing.

From above, we see a diagonal streak of collisions from the top right to the bottom left in the center of the graph. We also see collision orbits surrounding it, which, from the oirignal phase space graph, forms the periodic patterns of collisions.

Similar to the analysis from Pattern 1, next we pick 1 parameter combination allow the "claw mark" streak of collisions, and 1 parameter combination that does not yield a collision. We also compare the former to the collisions found in the periodic patterns in Pattern 1 to see if there are any differences.

Pattern 2: Collision Orbit ($N_2 = 18.5 N_1, \theta = 8$)¶

We first look at a collision orbit allow the path of the "claw-mark streak". We pick $N_2 = 10.5 N_1$ and $\theta = 8 $.

In [13]:
# Parameters to vary
n2 = 10.5 * 1.6e-19  # Nucleus 2 charge
angle = 8  # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.4, T, 80)
# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  4.870e-19 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.392e-11 ... -4.561e-11 -4.594e-11]
            [ 0.000e+00  1.147e-13 ... -4.911e-11 -4.756e-11]
            [ 2.159e+06  2.013e+06 ... -1.561e+06 -1.298e+06]
            [ 3.034e+05  1.682e+05 ...  6.864e+06  6.801e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 92870
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? Yes

Figure 29. When $N_2 = 10.5 N_1, \theta = 8$, the trajectory made by the electron is similar to that of the periodic collision patterns.

Along the claw-mark, the electorn seems to trace a path similar to the periodic collision orbits. However, the electron exhibits much more drift, creating shape that is much less dense than the previous orbits; even after orbiting for the entire 2.5e-15s, we still see many "holes" in the electron path. We next plot the distances between the electron, $N_1$, and $N_2$.

In [14]:
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2) 


fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()

ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True) 
ax[0].legend(loc = "upper left")

ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1) 
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2") 
ax.scatter(sol.t[index], minimum, c = "crimson",  s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14) 
ax.grid(True) 
ax.legend(loc = "upper left")
Out[14]:
<matplotlib.legend.Legend at 0x12728aa90>
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Figure 30 a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). Similar to the previous orbits, we see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape.

From these subplots, we see that the general graph shows similarities to that of pattern 1: the electrons exhibit oscillatory patterns who amplitudes vary periodically. However, we notice three stark differences:

  1. Frequency of Oscillations: The average frequency of oscillations is much lower; we can see this from the Figure 30a on the left, where the spacing between each oscillation can be very visibe compared to the distances in the periodic collision patterns, which are so dense that the individual oscillation cannot be seen.
  2. Frequency of Amplitude Variation: In constrast, the average time it takes for the amplitude to grow and shrink back to its original value is much quicker; due to this, the graphs no longer look like oscillations packaged in a sinusoidal pattern, but in wavepackets.
  3. Maximum amplitude of distances: From Figure 30 c, the minimum distance between $N_2$ and the electron is nearly an order of magnitude smaller.

Next, we look at a non-collision orbit.

Pattern 2: Non-Collision Orbit ($N_2 = 8N_1$ and $\theta = 4$)¶

For this, we pick $N_2 = 8N_1$ and $\theta = 4$.

In [16]:
# Parameters to vary
n2 = 8 * 1.6e-19  # Nucleus 2 charge
angle = 4  # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.4, T, 80)
# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  5.313e-19 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.402e-11 ...  5.471e-11  5.410e-11]
            [ 0.000e+00  5.182e-14 ... -8.240e-12 -8.760e-12]
            [ 2.175e+06  2.050e+06 ... -2.067e+06 -2.150e+06]
            [ 1.521e+05  4.349e+04 ... -1.753e+06 -1.818e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 75362
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 31. When $N_2 = 8N_1, \theta = 4$, the path traced by the electron is very similar to the collision orbit, but the density of the shape it traces is much denser.

In [17]:
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2) 


fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()

ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1, 2) 
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True) 
ax[0].legend(loc = "upper left")

ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True) 
ax[1].legend(loc = "upper left")

fig, ax = plt.subplots(1) 
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2") 
ax.scatter(sol.t[index], minimum, c = "crimson",  s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14) 
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14) 
ax.grid(True) 
ax.legend(loc = "upper left")
Out[17]:
<matplotlib.legend.Legend at 0x1275d2970>
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Figure 32. When $N_2 = 8N_1$ and $\theta = 4$, the distance between the electron, $N_1$, and $N_2$ show very similar graphs, with oscillatory and wavepacket-like behaviour.

From these subplots, we see that the difference described in the collision orbit also applies to the non-collision orbit. Hence, it is not due to these attributes that a clow-mark collision streak is seen. However, the minimum distance between $N_2$ and the electron is an entire two orders of magnitude less than the collision orbit. So again, the collision orbit is simply due to the slight variances in amplitudes in the distances between $N_2$ and the electron, but not due to complete differences in orbital trajectory shapes.

Pattern 3: Neon Band above Lower Third Region¶

Neon2

Next, we look at the beon band above the lower third region of the phase space analysis. From above, we see that there are very distinct changes in stability, from purple to green to neon yellow; here, we try to understand why this might be the case. Again, we first conduct a partial phase space analysis to zoom into this region.

In [19]:
import numpy as np 

print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

theta_min = 0
theta_max = 5
n_min = 0.05
n_max = 0.15

results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)

# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)

# Save results
np.savez('partial_phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 50 points from 0° to 90°
N2 axis: 50 points from N1/100 to 100*N1
  N2/N1 range: 0.050 to 0.1
Total simulations: 2500
======================================================================


N2/N1 = 0.050 (1/50)
  Progress: 50/2500 (2.0%) | Rate: 19.3 sim/s | ETA: 127s
N2/N1 = 0.052 (2/50)
  Progress: 100/2500 (4.0%) | Rate: 18.6 sim/s | ETA: 129s
N2/N1 = 0.054 (3/50)
  Progress: 150/2500 (6.0%) | Rate: 18.7 sim/s | ETA: 126s
N2/N1 = 0.056 (4/50)
  Progress: 200/2500 (8.0%) | Rate: 18.1 sim/s | ETA: 127s
N2/N1 = 0.058 (5/50)
  Progress: 250/2500 (10.0%) | Rate: 17.7 sim/s | ETA: 127s
N2/N1 = 0.060 (6/50)
  Progress: 300/2500 (12.0%) | Rate: 17.5 sim/s | ETA: 126s
N2/N1 = 0.062 (7/50)
  Progress: 350/2500 (14.0%) | Rate: 17.4 sim/s | ETA: 124s
N2/N1 = 0.064 (8/50)
  Progress: 400/2500 (16.0%) | Rate: 17.3 sim/s | ETA: 122s
N2/N1 = 0.066 (9/50)
  Progress: 450/2500 (18.0%) | Rate: 17.2 sim/s | ETA: 120s
N2/N1 = 0.068 (10/50)
  Progress: 500/2500 (20.0%) | Rate: 17.0 sim/s | ETA: 118s
N2/N1 = 0.070 (11/50)
  Progress: 550/2500 (22.0%) | Rate: 16.4 sim/s | ETA: 119s
N2/N1 = 0.072 (12/50)
  Progress: 600/2500 (24.0%) | Rate: 16.3 sim/s | ETA: 117s
N2/N1 = 0.074 (13/50)
  Progress: 650/2500 (26.0%) | Rate: 16.2 sim/s | ETA: 114s
N2/N1 = 0.077 (14/50)
  Progress: 700/2500 (28.0%) | Rate: 16.1 sim/s | ETA: 112s
N2/N1 = 0.079 (15/50)
  Progress: 750/2500 (30.0%) | Rate: 16.1 sim/s | ETA: 109s
N2/N1 = 0.081 (16/50)
  Progress: 800/2500 (32.0%) | Rate: 16.0 sim/s | ETA: 106s
N2/N1 = 0.083 (17/50)
  Progress: 850/2500 (34.0%) | Rate: 15.9 sim/s | ETA: 104s
N2/N1 = 0.085 (18/50)
  Progress: 900/2500 (36.0%) | Rate: 15.8 sim/s | ETA: 101s
N2/N1 = 0.087 (19/50)
  Progress: 950/2500 (38.0%) | Rate: 15.3 sim/s | ETA: 102s
N2/N1 = 0.089 (20/50)
  Progress: 1000/2500 (40.0%) | Rate: 15.2 sim/s | ETA: 99s
N2/N1 = 0.091 (21/50)
  Progress: 1050/2500 (42.0%) | Rate: 15.1 sim/s | ETA: 96s
N2/N1 = 0.093 (22/50)
  Progress: 1100/2500 (44.0%) | Rate: 14.9 sim/s | ETA: 94s
N2/N1 = 0.095 (23/50)
  Progress: 1150/2500 (46.0%) | Rate: 14.7 sim/s | ETA: 92s
N2/N1 = 0.097 (24/50)
  Progress: 1200/2500 (48.0%) | Rate: 14.6 sim/s | ETA: 89s
N2/N1 = 0.099 (25/50)
  Progress: 1250/2500 (50.0%) | Rate: 14.5 sim/s | ETA: 86s
N2/N1 = 0.101 (26/50)
  Progress: 1300/2500 (52.0%) | Rate: 14.3 sim/s | ETA: 84s
N2/N1 = 0.103 (27/50)
  Progress: 1350/2500 (54.0%) | Rate: 14.2 sim/s | ETA: 81s
N2/N1 = 0.105 (28/50)
  Progress: 1400/2500 (56.0%) | Rate: 14.1 sim/s | ETA: 78s
N2/N1 = 0.107 (29/50)
  Progress: 1450/2500 (58.0%) | Rate: 14.0 sim/s | ETA: 75s
N2/N1 = 0.109 (30/50)
  Progress: 1500/2500 (60.0%) | Rate: 14.0 sim/s | ETA: 72s
N2/N1 = 0.111 (31/50)
  Progress: 1550/2500 (62.0%) | Rate: 13.9 sim/s | ETA: 68s
N2/N1 = 0.113 (32/50)
  Progress: 1600/2500 (64.0%) | Rate: 13.8 sim/s | ETA: 65s
N2/N1 = 0.115 (33/50)
  Progress: 1650/2500 (66.0%) | Rate: 13.8 sim/s | ETA: 62s
N2/N1 = 0.117 (34/50)
  Progress: 1700/2500 (68.0%) | Rate: 13.7 sim/s | ETA: 58s
N2/N1 = 0.119 (35/50)
  Progress: 1750/2500 (70.0%) | Rate: 13.7 sim/s | ETA: 55s
N2/N1 = 0.121 (36/50)
  Progress: 1800/2500 (72.0%) | Rate: 13.6 sim/s | ETA: 52s
N2/N1 = 0.123 (37/50)
  Progress: 1850/2500 (74.0%) | Rate: 13.5 sim/s | ETA: 48s
N2/N1 = 0.126 (38/50)
  Progress: 1900/2500 (76.0%) | Rate: 13.4 sim/s | ETA: 45s
N2/N1 = 0.128 (39/50)
  Progress: 1950/2500 (78.0%) | Rate: 13.4 sim/s | ETA: 41s
N2/N1 = 0.130 (40/50)
  Progress: 2000/2500 (80.0%) | Rate: 13.3 sim/s | ETA: 38s
N2/N1 = 0.132 (41/50)
  Progress: 2050/2500 (82.0%) | Rate: 13.2 sim/s | ETA: 34s
N2/N1 = 0.134 (42/50)
  Progress: 2100/2500 (84.0%) | Rate: 13.2 sim/s | ETA: 30s
N2/N1 = 0.136 (43/50)
  Progress: 2150/2500 (86.0%) | Rate: 13.1 sim/s | ETA: 27s
N2/N1 = 0.138 (44/50)
  Progress: 2200/2500 (88.0%) | Rate: 13.0 sim/s | ETA: 23s
N2/N1 = 0.140 (45/50)
  Progress: 2250/2500 (90.0%) | Rate: 12.9 sim/s | ETA: 19s
N2/N1 = 0.142 (46/50)
  Progress: 2300/2500 (92.0%) | Rate: 12.9 sim/s | ETA: 16s
N2/N1 = 0.144 (47/50)
  Progress: 2350/2500 (94.0%) | Rate: 12.8 sim/s | ETA: 12s
N2/N1 = 0.146 (48/50)
  Progress: 2400/2500 (96.0%) | Rate: 12.7 sim/s | ETA: 8ss
N2/N1 = 0.148 (49/50)
  Progress: 2450/2500 (98.0%) | Rate: 12.7 sim/s | ETA: 4s
N2/N1 = 0.150 (50/50)
  Progress: 2500/2500 (100.0%) | Rate: 12.7 sim/s | ETA: 0s

Scan completed in 197.7s (3.3 min)
Average: 0.08s per simulation

Results Summary:
  Valid orbits:         2445 (97.8%)
  Collisions:             55 (2.2%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.000001
  Max:     0.772480
  Mean:    0.592599
  Median:  0.639730
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 33. Partial phase space graph of the domain $0 \leq \theta \leq 5$ and $0 \leq \frac{N_2}{N_1} \leq 0.15$. Stark differences in relative standard deviation of the period ranging from 0 to 0.7.

From this partial phase space, we see in further detail that there is very stark differences in relative standard deviation of the period, with horizontal regions of solid colour. Crucially, these regions of distinct colour do not fade into one another, indicating a gradual chnage in stability. Instead, we see very sudden streaks of colour. To understand this more detail, we pick three diffierent parameter combinations to analyze: one with a relative standard deviation of 0-0.1 (purple), one from 0.4-0.6 (green), and one higher than 0.7 (yellow).

Pattern 3: Relative Standard Deviation 0-0.1 ($N_2 = 0.02N_1, \theta = 5$)¶

We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.

In [58]:
# Parameters to vary
n2 = 0.02 * 1.6e-19  # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-15e-11, 30e-11)
    a.set_xlim(-3e-10,3e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.512e-18 ...  2.500e-14  2.500e-14]
        y: [[ 5.290e-11  5.615e-11 ...  8.280e-11  8.128e-11]
            [ 0.000e+00  3.196e-13 ... -7.309e-13 -6.788e-13]
            [ 2.172e+06  2.132e+06 ... -1.824e+06 -1.840e+06]
            [ 1.900e+05  2.322e+05 ...  5.710e+04  6.880e+04]]
      sol: None
 t_events: None
 y_events: None
     nfev: 56468
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 34. When $N_2 = 0.02 N_1, \theta = 5$, we see trajectory where the electron only orbits $N_1$ in an off-center elliptical path, with drift causing it to form a shell-like shape.

From these plots, we see that the electron orbits $N_1$ ellptically, with e drift to the left; this creates a shell-like shape, which would eventually form a half-sphere. To understand the relative standard deviation more thoroughly, we look at the histogram of periodicities.

In [60]:
# Set up initial conditions


angle = 5
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 2.5e-15  # time for simulation to run, s

# Set n2 as global for diff_eqns to use
n2 = 0.02 * 1.6e-19

# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 2
Mean period: 7.36e-16 s
Standard deviation: 1.55e-19 s
Relative std dev: 0.02%
Minimum period: 7.36e-16 s
Maximum period: 7.36e-16 s
Range: 3.11e-19 s 

Is the orbit periodic? Yes
==================================================
No description has been provided for this image

Figure 35. Histogram of orbital periods when $N_2 = 0.02 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 0.02%.

From the above, we see that the relative standard deviation is 0.02%. This is compatible with Figure 34, where we see that while the electron experiences drift, its orbital shape generally does not change.

Pattern 3: Relative Standard Deviation 0.4-0.6 ($N_2 = 0.08N_1, \theta = 5$)¶

We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.

In [64]:
# Parameters to vary
n2 = 0.08 * 1.6e-19  # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-15e-11, 30e-11)
    a.set_xlim(-3e-10,3e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.505e-18 ...  2.500e-14  2.500e-14]
        y: [[ 5.290e-11  5.614e-11 ... -2.257e-10 -2.255e-10]
            [ 0.000e+00  3.160e-13 ...  1.164e-10  1.182e-10]
            [ 2.172e+06  2.130e+06 ...  4.853e+04  6.569e+04]
            [ 1.900e+05  2.295e+05 ...  4.908e+05  4.861e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 57020
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 36. When $N_2 = 0.02 N_1, \theta = 5$. A similar shape is traced, where the electron ellptically orbits $N_1$ but drifts so that its trajectory forms a half-spherical shape.

From above, we see that the electron orbital trajectory is very similar to the first combination of parameters. However, we notice several differences:

  1. Drift: The electron experiences much more drift with this parameter combination. By the end of the time span, we see that the electron has already traced an entire half-sphere, which is not seen in Figure 34.
  2. Orbital SHape Variation: As the electron experiences high drift, the eccentricity of the electron's orbits also changes drastically. From the first subplot, we see that the electron's path transforms from a very eccentric elliptical orbit to a much lesser one within 1/8 of its time span. This could possibly affect the variations of orbital periods drastically. We confirm this below with our histograms of orbital periods.
In [63]:
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 2.5e-15  # time for simulation to run, s


# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 4
Mean period: 4.31e-16 s
Standard deviation: 2.31e-16 s
Relative std dev: 53.50%
Minimum period: 3.22e-17 s
Maximum period: 5.74e-16 s
Range: 5.42e-16 s 

Is the orbit periodic? No
==================================================
No description has been provided for this image

Figure 37. Histogram of orbital periods when $N_2 = 0.08 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 53.5%.

As seen from the histogram, the relative standard devaition has skyrocketed to 53.5%, most likely due both to the electron drift and its change in eccentricity in orbits. This explains why there is a stark difference in relative standard deviation in the original phase space graph. We expect the difference to be even more pronounced when charge ratio is slightly higher; we check this in our third iteration.

Pattern 3: Relative Standard Deviation > 0.7 ($N_2 = 0.115N_1, \theta = 5$)¶

We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.

In [73]:
# Parameters to vary
n2 = 0.115 * 1.6e-19  # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-15e-11, 30e-11)
    a.set_xlim(-3e-10,3e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.504e-18 ...  2.500e-14  2.500e-14]
        y: [[ 5.290e-11  5.613e-11 ...  2.208e-11  1.918e-11]
            [ 0.000e+00  3.147e-13 ...  1.741e-12  1.195e-12]
            [ 2.172e+06  2.128e+06 ... -2.539e+06 -2.566e+06]
            [ 1.900e+05  2.280e+05 ... -5.106e+05 -4.476e+05]]
      sol: None
 t_events: None
 y_events: None
     nfev: 65420
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 38. When $N_2 = 0.02 N_1, \theta = 5$, the same half=spherical shape is traced, with an even larger electron drift and change in eccentric orbit.

In [74]:
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 2.5e-15  # time for simulation to run, s


# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 5
Mean period: 3.07e-16 s
Standard deviation: 2.25e-16 s
Relative std dev: 73.30%
Minimum period: 3.16e-17 s
Maximum period: 5.08e-16 s
Range: 4.77e-16 s 

Is the orbit periodic? No
No description has been provided for this image

Figure 39. Histogram of orbital periods when $N_2 = 0.115 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 73.3%.

With the relative standard deviation being 73.3%, we see that this is indeed the case.

Hence, the neon bands above the third region occur most likely due to an increase in electron drift, as well as increased change in orbital eccentricities.

Pattern 4: Curves of Periodicity¶

Periodicity

Finally, we look at the curve of periodicity above. We first conduct a partial phase space scan of the region.

In [75]:
import numpy as np 

print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

theta_min = 32
theta_max = 37
n_min = 0.5
n_max = 1

results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)

# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)

# Save results
np.savez('partial_phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 50 points from 0° to 90°
N2 axis: 50 points from N1/100 to 100*N1
  N2/N1 range: 0.500 to 1.0
Total simulations: 2500
======================================================================


N2/N1 = 0.500 (1/50)
  Progress: 45/2500 (1.8%) | Rate: 7.1 sim/s | ETA: 345s
N2/N1 = 0.510 (2/50)
  Progress: 90/2500 (3.6%) | Rate: 7.1 sim/s | ETA: 341s
N2/N1 = 0.520 (3/50)
  Progress: 150/2500 (6.0%) | Rate: 7.2 sim/s | ETA: 327s
N2/N1 = 0.531 (4/50)
  Progress: 195/2500 (7.8%) | Rate: 7.1 sim/s | ETA: 325s
N2/N1 = 0.541 (5/50)
  Progress: 240/2500 (9.6%) | Rate: 7.0 sim/s | ETA: 322s
N2/N1 = 0.551 (6/50)
  Progress: 300/2500 (12.0%) | Rate: 7.1 sim/s | ETA: 312s
N2/N1 = 0.561 (7/50)
  Progress: 345/2500 (13.8%) | Rate: 7.0 sim/s | ETA: 307s
N2/N1 = 0.571 (8/50)
  Progress: 390/2500 (15.6%) | Rate: 7.0 sim/s | ETA: 302s
N2/N1 = 0.582 (9/50)
  Progress: 450/2500 (18.0%) | Rate: 7.0 sim/s | ETA: 294s
N2/N1 = 0.592 (10/50)
  Progress: 495/2500 (19.8%) | Rate: 6.9 sim/s | ETA: 290s
N2/N1 = 0.602 (11/50)
  Progress: 540/2500 (21.6%) | Rate: 6.9 sim/s | ETA: 286s
N2/N1 = 0.612 (12/50)
  Progress: 600/2500 (24.0%) | Rate: 6.8 sim/s | ETA: 279s
N2/N1 = 0.622 (13/50)
  Progress: 645/2500 (25.8%) | Rate: 6.8 sim/s | ETA: 273s
N2/N1 = 0.633 (14/50)
  Progress: 690/2500 (27.6%) | Rate: 6.7 sim/s | ETA: 269s
N2/N1 = 0.643 (15/50)
  Progress: 750/2500 (30.0%) | Rate: 6.7 sim/s | ETA: 262s
N2/N1 = 0.653 (16/50)
  Progress: 795/2500 (31.8%) | Rate: 6.6 sim/s | ETA: 257s
N2/N1 = 0.663 (17/50)
  Progress: 840/2500 (33.6%) | Rate: 6.6 sim/s | ETA: 252s
N2/N1 = 0.673 (18/50)
  Progress: 900/2500 (36.0%) | Rate: 6.5 sim/s | ETA: 245s
N2/N1 = 0.684 (19/50)
  Progress: 945/2500 (37.8%) | Rate: 6.5 sim/s | ETA: 239s
N2/N1 = 0.694 (20/50)
  Progress: 990/2500 (39.6%) | Rate: 6.5 sim/s | ETA: 234s
N2/N1 = 0.704 (21/50)
  Progress: 1050/2500 (42.0%) | Rate: 6.4 sim/s | ETA: 226s
N2/N1 = 0.714 (22/50)
  Progress: 1095/2500 (43.8%) | Rate: 6.4 sim/s | ETA: 220s
N2/N1 = 0.724 (23/50)
  Progress: 1140/2500 (45.6%) | Rate: 6.3 sim/s | ETA: 215s
N2/N1 = 0.735 (24/50)
  Progress: 1200/2500 (48.0%) | Rate: 6.3 sim/s | ETA: 207s
N2/N1 = 0.745 (25/50)
  Progress: 1245/2500 (49.8%) | Rate: 6.2 sim/s | ETA: 201s
N2/N1 = 0.755 (26/50)
  Progress: 1290/2500 (51.6%) | Rate: 6.2 sim/s | ETA: 195s
N2/N1 = 0.765 (27/50)
  Progress: 1350/2500 (54.0%) | Rate: 6.2 sim/s | ETA: 186s
N2/N1 = 0.776 (28/50)
  Progress: 1395/2500 (55.8%) | Rate: 6.1 sim/s | ETA: 180s
N2/N1 = 0.786 (29/50)
  Progress: 1440/2500 (57.6%) | Rate: 6.1 sim/s | ETA: 174s
N2/N1 = 0.796 (30/50)
  Progress: 1500/2500 (60.0%) | Rate: 6.0 sim/s | ETA: 165s
N2/N1 = 0.806 (31/50)
  Progress: 1545/2500 (61.8%) | Rate: 6.0 sim/s | ETA: 159s
N2/N1 = 0.816 (32/50)
  Progress: 1590/2500 (63.6%) | Rate: 6.0 sim/s | ETA: 153s
N2/N1 = 0.827 (33/50)
  Progress: 1650/2500 (66.0%) | Rate: 5.9 sim/s | ETA: 144s
N2/N1 = 0.837 (34/50)
  Progress: 1695/2500 (67.8%) | Rate: 5.9 sim/s | ETA: 137s
N2/N1 = 0.847 (35/50)
  Progress: 1740/2500 (69.6%) | Rate: 5.9 sim/s | ETA: 130s
N2/N1 = 0.857 (36/50)
  Progress: 1800/2500 (72.0%) | Rate: 5.8 sim/s | ETA: 120s
N2/N1 = 0.867 (37/50)
  Progress: 1845/2500 (73.8%) | Rate: 5.8 sim/s | ETA: 113s
N2/N1 = 0.878 (38/50)
  Progress: 1890/2500 (75.6%) | Rate: 5.8 sim/s | ETA: 106s
N2/N1 = 0.888 (39/50)
  Progress: 1950/2500 (78.0%) | Rate: 5.7 sim/s | ETA: 96ss
N2/N1 = 0.898 (40/50)
  Progress: 1995/2500 (79.8%) | Rate: 5.7 sim/s | ETA: 89s
N2/N1 = 0.908 (41/50)
  Progress: 2040/2500 (81.6%) | Rate: 5.7 sim/s | ETA: 81s
N2/N1 = 0.918 (42/50)
  Progress: 2100/2500 (84.0%) | Rate: 5.6 sim/s | ETA: 71s
N2/N1 = 0.929 (43/50)
  Progress: 2145/2500 (85.8%) | Rate: 5.6 sim/s | ETA: 64s
N2/N1 = 0.939 (44/50)
  Progress: 2190/2500 (87.6%) | Rate: 5.5 sim/s | ETA: 56s
N2/N1 = 0.949 (45/50)
  Progress: 2250/2500 (90.0%) | Rate: 5.5 sim/s | ETA: 45s
N2/N1 = 0.959 (46/50)
  Progress: 2295/2500 (91.8%) | Rate: 5.5 sim/s | ETA: 37s
N2/N1 = 0.969 (47/50)
  Progress: 2340/2500 (93.6%) | Rate: 5.4 sim/s | ETA: 30s
N2/N1 = 0.980 (48/50)
  Progress: 2400/2500 (96.0%) | Rate: 5.4 sim/s | ETA: 19s
N2/N1 = 0.990 (49/50)
  Progress: 2445/2500 (97.8%) | Rate: 5.4 sim/s | ETA: 10s
N2/N1 = 1.000 (50/50)
  Progress: 2500/2500 (100.0%) | Rate: 5.3 sim/s | ETA: 0s

Scan completed in 468.6s (7.8 min)
Average: 0.19s per simulation

Results Summary:
  Valid orbits:         2377 (95.1%)
  Collisions:            123 (4.9%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.000902
  Max:     0.612470
  Mean:    0.218956
  Median:  0.314808
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 40. Partial phase space graph when $32 \leq \theta \leq 37, 0.5 \leq \frac{N_2}{N_1} \leq 1$. Curve of periodicity seen surrounded by quasi-periodic, unstable orbits.

We pick one stable orbit and one unstable orbit to analyze their differences.

Pattern 4: Stable Orbit ($N_2 = 0.8N_1, \theta = 36$)¶

In [83]:
# Parameters to vary
n2 = 0.7 * 1.6e-19  # Nucleus 2 charge
angle = 35 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.4) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-15e-11, 15e-11)
    a.set_xlim(-1.25e-10,1.25e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  2.010e-18 ...  2.500e-14  2.500e-14]
        y: [[ 5.290e-11  5.640e-11 ... -6.273e-11 -6.283e-11]
            [ 0.000e+00  2.531e-12 ... -9.004e-11 -8.944e-11]
            [ 1.786e+06  1.697e+06 ... -2.440e+05 -2.287e+05]
            [ 1.250e+06  1.269e+06 ...  1.465e+06  1.475e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 170804
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 41. When $N_2 = 0.7N_1, \theta = 35$, the electron follows a figure-eight pattern, with its drift forming an ellipsoid-like shape.

We now compare this to an unstable orbit.

Pattern 4: Unstable Orbit ($N_2 = 0.6N_1, \theta = 32$)¶

In [89]:
# Parameters to vary
n2 = 0.6 * 1.6e-19  # Nucleus 2 charge
angle = 32 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.45) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-15e-11, 15e-11)
    a.set_xlim(-1.5e-10,1.5e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  2.156e-18 ...  2.499e-15  2.500e-15]
        y: [[ 5.290e-11  5.679e-11 ... -8.746e-11 -8.848e-11]
            [ 0.000e+00  2.518e-12 ...  3.847e-11  4.047e-11]
            [ 1.849e+06  1.759e+06 ... -7.486e+05 -6.979e+05]
            [ 1.155e+06  1.181e+06 ...  1.413e+06  1.414e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 15374
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 42. When $N_2 = 0.6N_1, \theta = 32$, the electron follows a completely different pattern, but still ultimately traces an ellipsoid-like shape. Time span is reduced to see orbital path more clearly.

Comparing this to Figure 41, we see that although the electron's path ultimately still creates an ellipsoid-like shape, its orbital path is drastically different. On one hand, in the stable orbit, the electron follows a figure-eight like pattern, with slight drift, creating an ellipsoid; crucially, because the drift, we see that the orbital shapes are roughly the same, contributing to a low relative standard deviation. On the other hand, in the unstable orbit, the electron seems to circle around $N_1$ and $N_2$ in a on on the right side, with its orbital shape growing rapidly to produce an ellipsoid. As such, it is reasonable for hte orbital period relative standard deviation be much larger.

Hence, we attribute this pattern to the rate of drift change the electron undergoes.

Part IV: Analyzing Numerical Artifacts and Quantification of Stability¶

Artifact

In our final section, we analyze some of the numeritcal artifacts that are seen in our continuous phase space scan. We specifically look at the above region, where we see unexpected shifts in color that do not align with the curves seen in the broader graph. We first conduct a partial phase space scan of the region.

In [91]:
import numpy as np 

print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")

theta_min = 70
theta_max = 90
n_min = 0.05
n_max = 0.1

results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)

# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)

# Save results
np.savez('partial_phase_space_heatmap_results.npz', 
         results_grid=results_grid,
         theta_range=theta_range,
         n2_range=n2_range,
         n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
======================================================================
======================================================================
PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability
======================================================================
Theta axis: 50 points from 0° to 90°
N2 axis: 50 points from N1/100 to 100*N1
  N2/N1 range: 0.050 to 0.1
Total simulations: 2500
======================================================================


N2/N1 = 0.050 (1/50)
  Progress: 45/2500 (1.8%) | Rate: 18.2 sim/s | ETA: 135s
N2/N1 = 0.051 (2/50)
  Progress: 90/2500 (3.6%) | Rate: 18.1 sim/s | ETA: 133s
N2/N1 = 0.052 (3/50)
  Progress: 150/2500 (6.0%) | Rate: 18.0 sim/s | ETA: 130s
N2/N1 = 0.053 (4/50)
  Progress: 195/2500 (7.8%) | Rate: 17.9 sim/s | ETA: 128s
N2/N1 = 0.054 (5/50)
  Progress: 240/2500 (9.6%) | Rate: 17.9 sim/s | ETA: 126s
N2/N1 = 0.055 (6/50)
  Progress: 300/2500 (12.0%) | Rate: 17.6 sim/s | ETA: 125s
N2/N1 = 0.056 (7/50)
  Progress: 345/2500 (13.8%) | Rate: 16.8 sim/s | ETA: 129s
N2/N1 = 0.057 (8/50)
  Progress: 390/2500 (15.6%) | Rate: 16.7 sim/s | ETA: 127s
N2/N1 = 0.058 (9/50)
  Progress: 450/2500 (18.0%) | Rate: 16.3 sim/s | ETA: 125s
N2/N1 = 0.059 (10/50)
  Progress: 495/2500 (19.8%) | Rate: 16.1 sim/s | ETA: 124s
N2/N1 = 0.060 (11/50)
  Progress: 540/2500 (21.6%) | Rate: 16.0 sim/s | ETA: 122s
N2/N1 = 0.061 (12/50)
  Progress: 600/2500 (24.0%) | Rate: 15.9 sim/s | ETA: 119s
N2/N1 = 0.062 (13/50)
  Progress: 645/2500 (25.8%) | Rate: 15.9 sim/s | ETA: 117s
N2/N1 = 0.063 (14/50)
  Progress: 690/2500 (27.6%) | Rate: 15.8 sim/s | ETA: 115s
N2/N1 = 0.064 (15/50)
  Progress: 750/2500 (30.0%) | Rate: 15.5 sim/s | ETA: 113s
N2/N1 = 0.065 (16/50)
  Progress: 795/2500 (31.8%) | Rate: 15.3 sim/s | ETA: 111s
N2/N1 = 0.066 (17/50)
  Progress: 840/2500 (33.6%) | Rate: 15.3 sim/s | ETA: 108s
N2/N1 = 0.067 (18/50)
  Progress: 900/2500 (36.0%) | Rate: 15.3 sim/s | ETA: 105s
N2/N1 = 0.068 (19/50)
  Progress: 945/2500 (37.8%) | Rate: 15.2 sim/s | ETA: 102s
N2/N1 = 0.069 (20/50)
  Progress: 990/2500 (39.6%) | Rate: 15.2 sim/s | ETA: 99ss
N2/N1 = 0.070 (21/50)
  Progress: 1050/2500 (42.0%) | Rate: 15.3 sim/s | ETA: 95s
N2/N1 = 0.071 (22/50)
  Progress: 1095/2500 (43.8%) | Rate: 15.3 sim/s | ETA: 92s
N2/N1 = 0.072 (23/50)
  Progress: 1140/2500 (45.6%) | Rate: 15.3 sim/s | ETA: 89s
N2/N1 = 0.073 (24/50)
  Progress: 1200/2500 (48.0%) | Rate: 15.3 sim/s | ETA: 85s
N2/N1 = 0.074 (25/50)
  Progress: 1245/2500 (49.8%) | Rate: 15.3 sim/s | ETA: 82s
N2/N1 = 0.076 (26/50)
  Progress: 1290/2500 (51.6%) | Rate: 15.3 sim/s | ETA: 79s
N2/N1 = 0.077 (27/50)
  Progress: 1350/2500 (54.0%) | Rate: 15.3 sim/s | ETA: 75s
N2/N1 = 0.078 (28/50)
  Progress: 1395/2500 (55.8%) | Rate: 15.3 sim/s | ETA: 72s
N2/N1 = 0.079 (29/50)
  Progress: 1440/2500 (57.6%) | Rate: 15.3 sim/s | ETA: 69s
N2/N1 = 0.080 (30/50)
  Progress: 1500/2500 (60.0%) | Rate: 15.3 sim/s | ETA: 65s
N2/N1 = 0.081 (31/50)
  Progress: 1545/2500 (61.8%) | Rate: 15.3 sim/s | ETA: 62s
N2/N1 = 0.082 (32/50)
  Progress: 1590/2500 (63.6%) | Rate: 15.3 sim/s | ETA: 59s
N2/N1 = 0.083 (33/50)
  Progress: 1650/2500 (66.0%) | Rate: 15.3 sim/s | ETA: 55s
N2/N1 = 0.084 (34/50)
  Progress: 1695/2500 (67.8%) | Rate: 15.3 sim/s | ETA: 53s
N2/N1 = 0.085 (35/50)
  Progress: 1740/2500 (69.6%) | Rate: 15.3 sim/s | ETA: 50s
N2/N1 = 0.086 (36/50)
  Progress: 1800/2500 (72.0%) | Rate: 15.3 sim/s | ETA: 46s
N2/N1 = 0.087 (37/50)
  Progress: 1845/2500 (73.8%) | Rate: 15.3 sim/s | ETA: 43s
N2/N1 = 0.088 (38/50)
  Progress: 1890/2500 (75.6%) | Rate: 15.3 sim/s | ETA: 40s
N2/N1 = 0.089 (39/50)
  Progress: 1950/2500 (78.0%) | Rate: 15.3 sim/s | ETA: 36s
N2/N1 = 0.090 (40/50)
  Progress: 1995/2500 (79.8%) | Rate: 15.3 sim/s | ETA: 33s
N2/N1 = 0.091 (41/50)
  Progress: 2040/2500 (81.6%) | Rate: 15.3 sim/s | ETA: 30s
N2/N1 = 0.092 (42/50)
  Progress: 2100/2500 (84.0%) | Rate: 15.3 sim/s | ETA: 26s
N2/N1 = 0.093 (43/50)
  Progress: 2145/2500 (85.8%) | Rate: 15.3 sim/s | ETA: 23s
N2/N1 = 0.094 (44/50)
  Progress: 2190/2500 (87.6%) | Rate: 15.3 sim/s | ETA: 20s
N2/N1 = 0.095 (45/50)
  Progress: 2250/2500 (90.0%) | Rate: 15.3 sim/s | ETA: 16s
N2/N1 = 0.096 (46/50)
  Progress: 2295/2500 (91.8%) | Rate: 15.2 sim/s | ETA: 13s
N2/N1 = 0.097 (47/50)
  Progress: 2340/2500 (93.6%) | Rate: 15.2 sim/s | ETA: 11s
N2/N1 = 0.098 (48/50)
  Progress: 2400/2500 (96.0%) | Rate: 15.2 sim/s | ETA: 7ss
N2/N1 = 0.099 (49/50)
  Progress: 2445/2500 (97.8%) | Rate: 15.2 sim/s | ETA: 4s
N2/N1 = 0.100 (50/50)
  Progress: 2500/2500 (100.0%) | Rate: 15.2 sim/s | ETA: 0s

Scan completed in 164.4s (2.7 min)
Average: 0.07s per simulation

Results Summary:
  Valid orbits:         2500 (100.0%)
  Collisions:              0 (0.0%)
  Errors:                  0 (0.0%)

Period Rel. Std. Dev. Statistics (valid orbits):
  Min:     0.000016
  Max:     0.888356
  Mean:    0.541145
  Median:  0.634151
======================================================================

Heat map saved as 'phase_space_heatmap_quantitative.png'
No description has been provided for this image
Results saved to 'lowphase_space_heatmap_results.npz'

Figure 43. Partial phase space simulation with the domain $70 \leq \theta \leq 90$, $0.05 \leq \frac{N_2}{N_1} \leq 0.1$. Possible numerical artifacts or shown.

Artifact2

From above, we see a partial phase space with possible numerical artifacts, characterized by sudden changes of colour. For instance, ther is an abrupt change in colour from pruple to green in the bottom of the graph. These results are different than the ones analyzed in Part III, since they do not follow any predictablw curve or pattern. We look more into the subregion above, picking one parameter combination in the green portion and one in the purple.

Green Portion: $N_2 = 0.065N_1, \theta = 82.5$¶

We first find a parameter combination in the green portion of the subregion. We pick $N_2 = 0.065N_1, \theta = 82.5$.

In [117]:
# Parameters to vary
n2 = 0.065 * 1.6e-19  # Nucleus 2 charge
angle = 82.5 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.35) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-25e-11, 25e-11)
    a.set_xlim(-2e-10,2e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.398e-18 ...  2.498e-15  2.500e-15]
        y: [[ 5.290e-11  5.327e-11 ... -2.582e-11 -2.428e-11]
            [ 0.000e+00  3.049e-12 ... -9.210e-11 -9.438e-11]
            [ 2.845e+05  2.442e+05 ...  7.957e+05  8.099e+05]
            [ 2.161e+06  2.201e+06 ... -1.210e+06 -1.176e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 5672
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 44. When $N_2 = 0.065 N_1, \theta = 82.5$ the electron follows elliptical paths orbiting around both $N_1$ and $N_2$, with drift shfting the ellptical orbits counterclockwise.

We also include a histogram of the orbital periods.

In [118]:
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 2.5e-15  # time for simulation to run, s


# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2e} s\n' \
             f'Rel. Std Dev: {period_std_rel:.2%}\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 4
Mean period: 4.61e-16 s
Standard deviation: 2.25e-16 s
Relative std dev: 48.81%
Minimum period: 7.14e-17 s
Maximum period: 5.95e-16 s
Range: 5.24e-16 s 

Is the orbit periodic? No
No description has been provided for this image

Figure 45. Histogram of orbital periods when $N_2 = 0.065 N_1, \theta = 82.5$. All orbital periods lie in two bins, with the relative standard deviation being 48.81%.

We do the exact same thing with a combination of parameters in the purple region.

Purple Portion: $N_2 = 0.05N_1, \theta = 82.5$¶

In [129]:
# Parameters to vary
n2 = 0.05 * 1.6e-19  # Nucleus 2 charge
angle = 82.5 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)


#Plot trajectory 

fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.35) 

ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
ax[2].set_aspect("equal") 
ax[2].set_title(f"Time Span = {T} s") 
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True) 
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[2].legend(loc='upper left', fontsize=9) 

# Get the one-third index
idx1 = len(sol1.t) // 3

# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal") 
ax[1].set_title(f"Time Span = (1/3) * {T} s") 
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True) 
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[1].legend(loc='upper left', fontsize=9) 

# Get the one-tenth index
idx2 = len(sol1.t) // 8

# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal") 
ax[0].set_title(f"Time Span = (1/8) * {T}s") 
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True) 
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
ax[0].legend(loc='upper left', fontsize=9) 

# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

# Apply limits to all subplots
for a in ax:
    a.set_ylim(-25e-11, 25e-11)
    a.set_xlim(-2e-10,2e-10)
plt.show()

# Set up initial conditions


theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 


# Run simulation
t_span = (0, T)

# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol) 
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}") 
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.396e-18 ...  2.500e-15  2.500e-15]
        y: [[ 5.290e-11  5.327e-11 ...  8.923e-11  8.923e-11]
            [ 0.000e+00  3.046e-12 ... -4.898e-11 -4.894e-11]
            [ 2.845e+05  2.448e+05 ... -2.004e+05 -2.007e+05]
            [ 2.161e+06  2.201e+06 ...  1.396e+06  1.396e+06]]
      sol: None
 t_events: None
 y_events: None
     nfev: 4856
     njev: 0
      nlu: 0
No description has been provided for this image
Is this a collision orbit? No

Figure 46. A similar trajectory can be seen in the purple region when $N_2 = 0.05N_1, \theta = 82.5$ .

In [130]:
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0) 

T = 2.5e-15  # time for simulation to run, s


# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0, 
rtol=1e-9, atol=1e-9, 
max_step=1e-17) 

# Check Periodicity
is_periodic = check_periodicity(sol)

# Generate Graph of Periods  
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)

# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)

# Calculate periods
periods = np.diff(sol.t[peaks])

# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean 

n = []
for i in range(len(periods)):
    n.append(i+1)



# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)

# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6", 
                                  rwidth=0.85, label='Orbital Periods', 
                                  edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2, 
            label=f'Mean Period = {period_mean:.2e} s') 



stats_text = f'Statistics:\n' \
             f'Mean: {period_mean:.2e} s\n' \
             f'Std Dev: {period_std:.2g} s\n' \
             f'Rel. Std Dev: {period_std_rel * 100:.2g} %\n' \
             f'Min: {np.min(periods):.2e} s\n' \
             f'Max: {np.max(periods):.2e} s\n' \
             f'N orbits: {len(periods)}'

props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes, 
         fontsize=10, verticalalignment='top', horizontalalignment='right',
         bbox=props, family='monospace')

# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

plt.axvspan(period_mean - period_std, period_mean + period_std, 
            alpha=0.2, color='orange', label='±1σ range')

# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)

plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)

print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
==================================================
ORBITAL PERIOD STATISTICS
==================================================
Number of complete orbits: 2
Mean period: 6.34e-16 s
Standard deviation: 1.12e-20 s
Relative std dev: 0.00%
Minimum period: 6.34e-16 s
Maximum period: 6.34e-16 s
Range: 2.24e-20 s 

Is the orbit periodic? Yes
No description has been provided for this image

Figure 47. Histogram of orbital periods when $N_2 = 0.05N_1, \theta = 82.5$. All orbital periods lie in two bins, with the relative standard deviation being 0.0018%.

Comparing the green and purple regions, we see that the green region has a stnadard deviation of nearly 50%, while the purple region has eseentially no standard deviation. In Figure 45, we see that the reason the standard deviation is so high is due to one occurrence of an orbital period being close to 1e-16 seconds. This is more than five times less than the other periods. Why might this be? Looking at the orbital paths in the green region (Figure 44), a possible explanation for this is the last orbit the electron makes around $N_1$ and $N_2$ in the time span. In the last orbit, the electron first travels near $N_1$, yielding a local minima; it then travels past $N_1$, turns around, and travels downwards towards $N_2$. Because of the unique eccentricity of the electron's elliptical path, another local minima is measured as the electron switches direction approaches $N_2$. As a result, a very brief period is detected by the function that quantifies stability, even though it should not. This is most likely why the standard deviation is so large in the green region. Since the green region is due to a detection of an inaccurate period, we can classify this occurrence in our phase space as a numerical artifact.

More generally, this analysis shows that there are limitations in this project's quantification of stability. Specifically, there are occurrences where orbital periods are detected that do not exist and vice versa. This will affect the accuracy of the standard deviation calculated by the simulation, which in turns affects the accuracy of the continuous phase space simulation.

Results and Discussion¶

Part I General Analysis¶

From Part I, electrons create four kinds of trajectories: ellipsoids, partial/half ellipsoids, rings, and flat elliptical rings. These shapes are traced by various periodic and aperiodic electron paths, ranging from infinity rings to ellipses to other forms of loops

  • From Cycle 1, $N_2 = N_1$: The electrons trace a 3-dimensional ellipsoid, with path densities changing due to differences in $\theta$. The trajectory of the electron is roughly symmetric, which is expected since we have set $N_1 = N_2$.
  • From Cycle 2 $N_2 = 5 N_1$: The electrons trace a partial ellipsoid due to the larger $N_2$ force, with the curving of the partial ellipsoid increasing as $\theta$ approaches 90 degrees. This is due to the vertical initial velocity component; as $\theta$ nears 90, the y-velocity increases, allowing the electron to travel further towards $N_1$ before its direction reverses towards $N_2$. In Cycle 2, the force of $N_1$ is still relatively large enough to influence the path of the electron, as seen by the elliptical shape of the trajectory; if there were no $N_1$ influence, the electron should trace a flat elliptical pattern, as seen in Cycle 4.
  • From Cycle 3 $N_2 = \frac{1}{5}N_1$: The electrons range between tracing ring-like shapes and partial ellipsoids, depending on $\theta$. As $\theta$ approaches 90 degrees, the electrons have enough vertical velocity to trace a ring-like shape; this occurs due to the low $N_2$ force, allowing the electrons to pass" $N_1$ and $N_2$ before reversing direction towards the other proton. As $\theta$ moves away from 90, the decrease in vertical component velocity gradually increases the thickness of the traced ring until the ring diverges into a partial ellipsoid, as seen in Cycle 2.
  • From Cycle 4: $N_2 = 50N_1$: The electron traces a flat elliptical path with drift, creating a rubber-band-like trajectory. This trajectory is different from that of Cycle 2, since the force of $N_2$ is magnitudes larger than $N_1$; now that $N_1$ has a negligible effect on the electron, there is no longer a partial ellipsoid. The electron still experiences drift due to the chosen initial velocity and variable angle; its orbit is non-Keplerian, meaning that its path will never be a two-dimensional ellipse.

Overall, Part I demonstrates that electrons exhibit a wide range of shapes and trajectories in response to varying nucleic charge and angle. This is due to the interactions between $N_2$ and $N_1$ Coulomb force, as well as the initial y and x velocity components from $\theta$.

Part II General Analysis¶

Discrete Phase-Space Diagram¶

No description has been provided for this image No description has been provided for this image No description has been provided for this image

Figure 22. Screenshots of three stability regions. From left to right: Low $\frac{N_2}{N_1}$ ratio, curving band, and chaotic region.

From Part II, there are three key regions where stable orbits are achieved:

  1. Low $\frac{N_2}{N_1}$ ratio: This is the region with the most prevalent stable orbits. When the charge ratio is between 0.01 and ~0.08, nearly all orbits are stable, with some exceptions when the initial velocity angle is between 60 and 70 degrees. Past this charge ratio, most orbits become either unstable due to lack of periodicity or collide into 1 of two protons, with the latter chance increasing as the charge ratio increases. Upon analyzing a stable orbit in this region, we see that the electrons follow an elliptical path with circular drift, creating a partial ellipsoid described in Cycle 2 of the Part I analysis.
  2. Curving Band: Above the low $\frac{N_2}{N_1}$ ratio region, we see a curving band of stable orbits. Upon analyzing a stable orbit in this region, we see that the electrons follow a figure-eight path, with its trajectory growing each cycle. The electron's path traces a full ellipsoid similar to the ones described in Cycle 1 of the Part I analysis.
  3. Chaotic Region: In the top right portion of the phase space diagram, there is a chaotic region of stable, unstable, and collision orbits. Upon analyzing a stable orbit in this region, we see that the electron traces a very similar orbit to Trial 2 of Cycle 1, where the electron follows a looping pattern up towards $N_1$ then down towards $N_2$, also tracing out an ellipsoid, but of a higher eccentricity. Interestingly, when we change $N_2$ by $0.005e-19C$, the orbit changes from stable to a collision with the nucleus. This shows how fragile the system in the chaotic region can be; just by varing $N_2$ by $0.03 \%$, the orbit transitions from a stable one to a collision wth the nucleus.

Part III and IV General Analyses¶

Continuous Phase-Space Heat Map¶

From the continuous phase-space heat map, we see all the patterns that the discrete phase-space diagram present with further detail. Three addition key observations are made. Firstly, there is a neon band of quasi-periodic orbits immediately above the lower third region, with a very stark contrast in relative standard deviations between two regions. Secondly, the collision patterns towards the top of the graphs show three swooping curves, along with repeating horizontal patterns. Finally, curves of quasi-periodicity can be seen in the middle of the graph, where streaks of turquoise and purple connect to the lower portion of the graph and transition to less stable orbits (yellow) as charge ratio decreases. A more detailed analysis of the patterns yields the following conclusions:

(1) Periodic and Claw-Mark Collisiond: The periodic and claw-mark collisions seen in the top of the continuous phase space diagrams are due to slight variations in amplitude of electron distances form $N_2$.

(2) Neon bands in lower third region: The abrupt neon bands above the lower region of stable orbits is most likely due to a combination of electron drift and its change in eccentricity in orbits. This in turn creates variations in the electron's orbital periods, contributing to a higher relative standard deviation.

(3) Curves of periodicity: The curves of periodicity in the center of the graph can be attributed to a low drift change of the electron's orbit, characterized by stable figure-eight like orbits

(4) Numerical artifacts: Numerical artifacts in the lower right portion of the phase space graph can be seen, where certain parameter combinations miscount the number of periods due to the quantification of stability through local minimum distances. This affects the accuracy of the simulation and may lead to unrepresentative results coming from our data.

Limitations & Next Steps¶

Technical Limitations¶

  • Machine Precision: Since we used Solve_IVP in this project, we will have limitations in machine precision– we are approximating the velocity and position by discretizing our calculations, reducing the accuracy of our simulation. While Solve_IVP is most likely more precise than simply using Euler's method, the function will still have uncertainty.
  • CPU Memory & Simplistic Model: Since we are running this project on a limited CPU memory, we cannot run each simulation for a very long period to more accurately determine stability. For context, the phase-space simulation in this project took nearly two hours to run on VS code! This limits the extent and complexity that we can make our simulations.
  • Approximating Periodicity: In this project, we approximated periodicity by taking the local minima of the distance between the electron and its original position. However, it is possible that this is not representative of the actual period; the electron may return to its original position, but that point does not mark a period, such as if it does multiple loops around a nucleus before completing a cycle. This is a technical limitation of our simulation, since its approximation of periodicity may not be accurate.
  • Other Stability Quantification Limitations: As discussed in Part IV of the analysis, the presence of numerical artifacts is due to limitations in the way this project defined stability. This means that some of the results seen in our phase spaces may not be truly representative of teh electron orbital trajectories.

Next Steps¶

  • Improving Phase Space Resolution: In our Part II analysis, we see that our phase space resolution is very low, even after running our phase space over 900 parameter combinations. Running the phase space over more combinations woudl increase the resolution of the graph, allowing us to observe more relevant patterns in the graph.
  • Better definition of stability: A more reliable quantifcation of stability could be developed so that there are very few (if not none) numerical artifacts in the simulated results.
  • Understanding mechanisms to orbital stability: Further effort could go into understanding why certain combinations of charge ratio and initial velocity angle produce stable orbits, while others do not.

Acknowledgements¶

  • I acknowledge the use of Claude AI for helping me develop the functions to extend my simulation code to scan for and plot my 2-D phase space.
  • I acknowledge Sumaiya Hussain and Sam Quastel for providing me with preliminary feedback on choosing a phase space for my project.

References¶

[1] https://en.wikipedia.org/wiki/Coulomb%27s_law

[2] https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html

Appendix 1: Code validation¶

A1.1: Check for Initial Velocity Angle, $\theta = 0$¶

Our first validation task is extremely brief, where we check that the initial velocity angle is implemented correctly. We specifically test $\theta = 0$ with $N_1 = N_2$, in which the electron's initial velocity will be to the right. Due to this, the forces of $N_2$ and $N_1$ on the electron will be purely horizontal, meaning that we expect the electron's trajectory to oscillate left and right.

In [ ]:
def plot_trajectory(sol1, top, T, c):
    # Plot y vs x for theta = 5
    fig, ax = plt.subplots(1, 3)
    fig.set_size_inches(14, 9)
    fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
    plt.subplots_adjust(top=top) 

    ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue") 
    ax[2].set_aspect("equal") 
    ax[2].set_title(f"Time Span = {T} s") 
    ax[2].set_xlabel("x (m)")
    ax[2].set_ylabel(" y (m) ")
    ax[2].grid(True) 
    ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[2].legend(loc='upper left', fontsize=9) 

    # Get the one-third index
    idx1 = len(sol1.t) // 3

    # Plot only the first one-third of the data
    ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
    ax[1].set_aspect("equal") 
    ax[1].set_title(f"Time Span = (1/3) * {T} s") 
    ax[1].set_xlabel("x (m)")
    ax[1].set_ylabel(" y (m) ")
    ax[1].grid(True) 
    ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[1].legend(loc='upper left', fontsize=9) 

    # Get the one-tenth index
    idx2 = len(sol1.t) // 8

    # Plot only the first one-third of the data
    ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
    ax[0].set_aspect("equal") 
    ax[0].set_title(f"Time Span = (1/8) * {T}s") 
    ax[0].set_xlabel("x (m)")
    ax[0].set_ylabel(" y (m) ")
    ax[0].grid(True) 
    ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1) 
    ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1) 
    ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)  
    ax[0].legend(loc='upper left', fontsize=9) 

    # Find global min and max for x and y
    x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
    y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()

    # Apply limits to all subplots

    plt.show()
In [ ]:
# Parameters to vary
n2 = 1.6e-19  # Nucleus 2 charge
angle = 0   # Launch angle in degrees
T = 1.5e-15 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.4, T, 0.8)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  2.193e-18 ...  1.499e-15  1.500e-15]
        y: [[ 5.290e-11  5.756e-11 ... -7.538e-11 -7.696e-11]
            [ 0.000e+00  0.000e+00 ...  0.000e+00  0.000e+00]
            [ 2.180e+06  2.067e+06 ... -1.615e+06 -1.573e+06]
            [ 0.000e+00  0.000e+00 ...  0.000e+00  0.000e+00]]
      sol: None
 t_events: None
 y_events: None
     nfev: 4028
     njev: 0
      nlu: 0
No description has been provided for this image

We see that this is indeed the case.

A1.2: Check for Extreme, $\frac{N_2}{N_1}$ Very large¶

Our second validation task will be to check for the electron trajectory when $\frac{N_2}{N_1}$ is very large. When $N_2$ is big, we expect that the orbital trajectory is a very eccentric ellipse where the electron purely orbits $N_2$, since the relative strength of $N_1$ on the electron is negligible. The force on the electron by $N_2$ should be so large that the electron's trajectory is nearly a straight line, indicating an eccentricity ~1. We use $N_2 = 5000N_1$, $\theta = 90$ to check this:

In [24]:
# Parameters to vary
n2 = 5000 * 1.6e-19  # Nucleus 2 charge
angle = 70   # Launch angle in degrees
T = 1.5e-18 # time for simulation to run, s

sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  7.464e-21 ...  1.496e-18  1.500e-18]
        y: [[ 5.290e-11  5.290e-11 ...  5.252e-11  5.248e-11]
            [ 0.000e+00  1.117e-14 ... -3.267e-13 -3.675e-13]
            [ 7.456e+05 -2.289e+05 ... -9.991e+06 -1.051e+07]
            [ 2.049e+06  9.436e+05 ... -1.008e+07 -1.067e+07]]
      sol: None
 t_events: None
 y_events: None
     nfev: 3230
     njev: 0
      nlu: 0
No description has been provided for this image

This is indeed what we see, indicating that our simulation is working as planned.

Appendix 2: Reflection questions¶

Reflection 1: Coding Approaches (A)¶

(How well did you apply and extend your coding knowledge in this project? Consider steps you took to make the code more efficient, readable and/or concise. Discuss any new-to-you coding techniques, functions or python packages that you learned how to use. Reflect on any unforeseen coding challenges you faced in completing this project.)

In this project, I extended my coding knowledge by learning to create functions in Python. By creating functions, I made my code more efficient and readable. I also got into the habit of writing code summaries for each of my code blocks, making them more readable. I extended my matplotlib abilities when I learned how to graph subplots and various titles in my Part I analysis. I also learned how to use Solve_IVP, which I had never used before.

Reflection 2: Coding Approaches (B)¶

(Highlight an aspect of your code that you feel you did particularily well. Discuss an aspect of your code that would benefit the most from further effort.)

Relating to reflection 1, I think my code organization was done very well. Especially for Part II, I created many functions needed in my simulation, and successfully integrated them all to generate an informative phase-space diagram. One thing that I could benefit the most from is to make my functions more compact. I currently have several functions that do the same thing with the outputs just being formatted differently, which is not very cofe-efficient. I should improve this by combining such functions into one.

Reflection 3: Simulation phyiscs and investigation (A)¶

(How well did you apply and extend your physical modelling and scientific investigation skills in this project? Consider the phase space you chose to explore and how throroughly you explored it. Consider how you translated physics into code and if appropriate any new physics you learned or developed a more thorough understanding of.)

I extended my physical modelling skills by learning how to make advanced phase-space diagrams. I previously had no idea how to use matplotlib to create heat maps and plots like the one created in Part II. However, with the help of AI and various documentations online, I was able to learn about all the functison matplotlib offers to create informative visuals. I also extended by knowledge about orbits through this project. I am currently taking ASTR 200, and a large part of the course is understanding Keplerian orbits using ellipses and eccentricity. In this project, I extended my knowledge to non-Keplerian orbits, where electrons trace non-elliptical trajectories, and even if they do, the electrons have various amounts of drift in their paths.

Reflection 4: Simulation phyiscs and investigation (B)¶

(Highlight something you feel you did particularily well in terms of the context of your simulation, the physical modelling that you did or the investigation you performed. Discuss an aspect of these dimensions of your project that would benefit the most from further effort.)

I think my Part I analysis was done particularly well because of my thoroughness. The orbits that I chose to show in my Part I analysis each show a distinct characteristics relating back to either nucleic charge or initial velocity angle, helping form an understanding abotu how these two parameters affect orbital stability. I think I was go more in-depth with my Part II analysis though, through investigating specific portions of my phase-space diagram and understanding the differences in orbital trajectories between the three identified regions (See "Limitations and Next Steps").

Reflection 5: Effectiveness of your communication¶

(Highlight something you feel you did particularily well in your visualizations or written communication. Discuss an aspect of your visualizations or written communication that would benefit the most from further effort.)

I think the introduction to my project was done really well! I laid out the groundwork of my project in a straightforward manner, and I think I included everything needed to properly understand my simulations. I could improve my making my code block summaries more informative.